From ac77db99e023d418f7c374422c77efe409510d3f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 14:38:59 +0000 Subject: [PATCH] Normalize English prose formatting across maintained files --- .github/workflows/ci.yml | 12 +- Gemfile | 5 +- README.md | 28 +-- Rakefile | 2 +- automatic.gemspec | 8 +- doc/AI_TUTORIAL.md | 31 ++-- doc/BASIC_DESIGN.md | 25 +-- doc/DEPLOYMENT.md | 72 ++++---- doc/PLUGINS.md | 180 ++++++++++--------- doc/PLUGIN_DEVELOPMENT.md | 2 +- doc/POLICY.md | 100 ++++++----- doc/QUICKSTART.md | 16 +- doc/RELEASING.md | 86 ++++----- doc/REQUIREMENTS.md | 42 ++--- lib/automatic.rb | 10 +- lib/automatic/cli.rb | 4 +- lib/automatic/environment.rb | 4 +- lib/automatic/http.rb | 3 +- lib/automatic/log.rb | 4 +- lib/automatic/pipeline.rb | 2 +- lib/automatic/recipe.rb | 6 +- lib/automatic/version.rb | 2 +- plugins/filter/claude.rb | 7 +- plugins/filter/description_link.rb | 2 +- plugins/filter/full_feed.rb | 4 +- plugins/filter/gemini.rb | 5 +- plugins/filter/github_feed.rb | 2 +- plugins/filter/image.rb | 3 +- plugins/filter/image_source.rb | 2 +- plugins/filter/join.rb | 10 +- plugins/filter/kimi.rb | 5 +- plugins/filter/limit.rb | 2 +- plugins/filter/open_ai.rb | 5 +- plugins/filter/sakura_ai.rb | 5 +- plugins/filter/sort.rb | 4 +- plugins/publish/hatena_bookmark.rb | 4 +- plugins/publish/markdown.rb | 4 +- plugins/store/digest.rb | 4 +- plugins/store/file.rb | 2 +- plugins/subscription/feed.rb | 2 +- plugins/subscription/tumblr.rb | 2 +- script/build | 2 +- spec/lib/automatic/cli_spec.rb | 2 +- spec/lib/automatic/pipeline_spec.rb | 4 +- spec/lib/automatic_spec.rb | 2 +- spec/lib/automatic_spec_helper_spec.rb | 2 +- spec/plugins/custom_feed/web_spec.rb | 2 +- spec/plugins/filter/claude_spec.rb | 2 +- spec/plugins/filter/description_link_spec.rb | 2 +- spec/plugins/filter/gemini_spec.rb | 2 +- spec/plugins/filter/kimi_spec.rb | 2 +- spec/plugins/filter/limit_spec.rb | 4 +- spec/plugins/filter/open_ai_spec.rb | 2 +- spec/plugins/filter/sakura_ai_spec.rb | 2 +- spec/plugins/filter/tumblr_resize_spec.rb | 2 +- spec/plugins/notify/ikachan_spec.rb | 2 +- spec/plugins/provide/fluentd_spec.rb | 4 +- spec/plugins/publish/amazon_s3_spec.rb | 2 +- spec/plugins/publish/eject_spec.rb | 2 +- spec/plugins/publish/fluentd_spec.rb | 2 +- spec/plugins/publish/instapaper_spec.rb | 4 +- spec/plugins/publish/memcached_spec.rb | 2 +- spec/plugins/store/file_spec.rb | 4 +- spec/spec_helper.rb | 4 +- 64 files changed, 402 insertions(+), 374 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 22c7a00..4f5fa41 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,7 @@ name: CI # Ruby versions this project validates continuously. # # The supported Ruby requirement lives in automatic.gemspec. The matrix -# below is the authoritative set validated continuously; see +# below is the authoritative set validated continuously. See # doc/POLICY.md section 11 and doc/REQUIREMENTS.md section 20. # # No credential is configured here and nothing reaches an external service: the @@ -14,8 +14,8 @@ name: CI # # This is the required check, and it is deliberately the minimal configuration: # what it proves on every commit is that the framework needs only the -# runtime dependencies declared in automatic.gemspec. The all-plugins configuration is a separate, -# non-required workflow, plugins.yml. +# runtime dependencies declared in automatic.gemspec. The all-plugins +# configuration is a separate, non-required workflow, plugins.yml. on: push: @@ -40,9 +40,9 @@ jobs: fail-fast: false matrix: # The continuously validated versions. This matrix is authoritative for - # that set; automatic.gemspec is authoritative for the supported Ruby - # requirement. Documentation refers to these sources rather than acting as - # a second matrix. + # that set, and automatic.gemspec is authoritative for the supported + # Ruby requirement. Documentation refers to these sources rather than + # acting as a second matrix. ruby: ['3.3', '3.4', '4.0'] steps: diff --git a/Gemfile b/Gemfile index 5b2ba2a..3db5b96 100644 --- a/Gemfile +++ b/Gemfile @@ -4,8 +4,9 @@ # here. See doc/POLICY.md section 9. # # `bundle install` with no configuration installs the framework's runtime -# dependencies declared in automatic.gemspec and the development ones, and nothing below: a checkout is set -# up to run and to test the framework, not to run every plugin. +# dependencies declared in automatic.gemspec and the development ones, and +# nothing below. A checkout is set up to run and to test the framework, not to +# run every plugin. source 'https://rubygems.org' diff --git a/README.md b/README.md index d1b95be..927d594 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ every time. it collected as a Markdown document: a person reads it as it is, `grep` searches it, Git keeps its history, and a program — a language model or an agent among them — takes it as input without a parser or an API. That is the general case, -and it needs no account anywhere; the plugins that publish to a service are for +and it needs no account anywhere. The plugins that publish to a service are for when a particular service is the point. Automatic Ruby is neither host infrastructure nor a purpose-built @@ -99,7 +99,7 @@ exactly three things: over, - **a loader** that finds a step by name, so that a Recipe can name it. -Everything else is a plugin. The framework is intentionally kept small; its +Everything else is a plugin. The framework is intentionally kept small. Its responsibilities and architectural boundary are defined in [`doc/BASIC_DESIGN.md`](doc/BASIC_DESIGN.md) and [`doc/POLICY.md`](doc/POLICY.md). @@ -109,7 +109,7 @@ accounts and their own files. It is not a service, and there is no notion of a second user. The project began in February 2012 and this is the first release since 2015. The -core, the Recipe format and the plugin contract are unchanged; what has changed +core, the Recipe format and the plugin contract are unchanged. What has changed is that it runs on a current Ruby, installs from a current RubyGems, and ships a plugin set every part of which still has somewhere to talk to. See [`doc/VERSIONS`](doc/VERSIONS). @@ -206,8 +206,8 @@ Two statements, and they are not the same one: - **Continuously validated versions.** The authoritative set is the matrix in [`.github/workflows/ci.yml`](.github/workflows/ci.yml), which represents the supported range rather than enumerating every intermediate release. A - version's absence from the matrix means it is not verified on every commit; - it does not mean it is expected to fail. + version's absence from the matrix means it is not verified on every commit. + It does not mean it is expected to fail. ## 5. Installation @@ -229,7 +229,8 @@ that plugin, with `gem install nokogiri` or `gem install activerecord sqlite3`. ### From a checkout Use a checkout to try the current development version, change the source, -develop a plugin or verify changes before a release. The supported checkout setups are shown below; start with the minimal setup. +develop a plugin or verify changes before a release. The supported checkout +setups are shown below. Start with the minimal setup. ```sh git clone https://github.com/id774/automaticruby.git @@ -263,7 +264,7 @@ the checkout resolves its gems through the bundle — so a plugin's gem is added with a group rather than with `gem install`, and a Recipe using plugins from two groups selects both at once. The group names, which plugin needs which gem, and a Recipe taken step by step through choosing its groups are in -[`doc/DEPLOYMENT.md`](doc/DEPLOYMENT.md); what each plugin does is in +[`doc/DEPLOYMENT.md`](doc/DEPLOYMENT.md), and what each plugin does is in [`doc/PLUGINS.md`](doc/PLUGINS.md). ## 6. Quick start @@ -420,7 +421,7 @@ self-contained contribution candidate. No plugin here is stubbed, mocked or simulated to make a test pass. Where a plugin's gem is not installed its spec is skipped and says which gem is -missing; where the plugin still loads, its spec covers what does not need the +missing. Where the plugin still loads, its spec covers what does not need the service. A dead integration is never made to look alive — it is removed. The contract, a worked example, and how to test a plugin are in @@ -519,7 +520,7 @@ defence, not the boundary.) | `~/.automatic/db` | SQLite databases the store plugins write. | | `~/.automatic/assets` | Data files plugins read, such as the fulltext siteinfo. | -Each part is optional; where one is absent, the corresponding directory inside +Each part is optional. Where one is absent, the corresponding directory inside the installation is used instead. `automatic scaffold` creates and seeds them and overwrites nothing, so it is safe to run after an upgrade. @@ -555,7 +556,7 @@ COVERAGE=on bundle exec rake spec ``` - A spec whose plugin needs a gem that is not installed is skipped, and says - which gem is missing. That absence is the signal; a plugin whose service no + which gem is missing. That absence is the signal. A plugin whose service no longer exists is never stubbed into passing. - `test/integration/` holds Recipes for exercising plugins against real services. They are run by hand, are not part of the suite, and are never run @@ -698,7 +699,7 @@ store databases — is listed in [`doc/POLICY.md`](doc/POLICY.md) section 7. The name **Automatic Ruby** was partly inspired by David Heinemeier Hansson's story of naming Ruby on Rails. He first chose *Rails* for the idea of putting -development "on rails"; when the obvious Rails domain names were unavailable, +development "on rails". When the obvious Rails domain names were unavailable, `rubyonrails.com` was free. He later said he liked the result because it put Ruby itself more prominently in the name and helped introduce Ruby to a wider audience. See [his account of the naming](https://tim.blog/2018/06/05/the-tim-ferriss-show-transcripts-david-dhh-heinemeier-hansson/). @@ -713,7 +714,10 @@ both, sounded natural enough, and was easy to remember. That was enough. This repository uses `master` as its primary branch name. -The name is used solely as a technical identifier, following the long-standing convention historically used by Git. It does not express or imply any association with racism, slavery, discrimination, or any political or social ideology. +The name is used solely as a technical identifier, following the long-standing +convention historically used by Git. It does not express or imply any +association with racism, slavery, discrimination, or any political or social +ideology. ## 19. License diff --git a/Rakefile b/Rakefile index ed233a7..3421a5c 100644 --- a/Rakefile +++ b/Rakefile @@ -9,7 +9,7 @@ # Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers. # # Test tasks only. This is not a build system, and the Jeweler and rcov tasks -# that used to live here have been removed; see doc/POLICY.md section 9.4. +# that used to live here have been removed. See doc/POLICY.md section 9.4. require 'rspec/core' require 'rspec/core/rake_task' diff --git a/automatic.gemspec b/automatic.gemspec index b92d290..673ede2 100644 --- a/automatic.gemspec +++ b/automatic.gemspec @@ -8,8 +8,8 @@ # Updated:: Aug 14, 2026 # Copyright:: Copyright (c) 2012-2026 Automatic Ruby Developers. # -# This file is maintained by hand. It was generated by Jeweler until v14.12.2; -# the generator is no longer maintained and the file is now source. See +# This file is maintained by hand. It was generated by Jeweler until v14.12.2, +# but the generator is no longer maintained and the file is now source. See # doc/POLICY.md section 9.4. Gem::Specification.new do |spec| @@ -44,11 +44,11 @@ Gem::Specification.new do |spec| # A lower bound only, and deliberately so. An upper bound would refuse a Ruby # this code has every reason to work on, on the day it is released, and the # only way to lift it would be a new release of this gem. What CI validates - # is a separate and narrower statement; see doc/REQUIREMENTS.md section 20. + # is a separate and narrower statement. See doc/REQUIREMENTS.md section 20. spec.required_ruby_version = '>= 3.3.0' # Shipped files. Derived from what Git sees, so that the list cannot drift - # from the repository; the Dir fallback is for building outside a checkout. + # from the repository. The Dir fallback is for building outside a checkout. # # --others --exclude-standard includes a file that has been created but not # yet added, while still honouring .gitignore. Listing only --cached silently diff --git a/doc/AI_TUTORIAL.md b/doc/AI_TUTORIAL.md index 7634820..37220fc 100644 --- a/doc/AI_TUTORIAL.md +++ b/doc/AI_TUTORIAL.md @@ -9,7 +9,7 @@ produces something a person can read. It assumes [`QUICKSTART.md`](QUICKSTART.md) has been followed once, so that `automatic` runs, `~/.automatic` exists, and the habit of asking "which plugins does this Recipe name, and what do they need" is already in place. What each -plugin does in full is [`PLUGINS.md`](PLUGINS.md) section 6; this document is +plugin does in full is [`PLUGINS.md`](PLUGINS.md) section 6. This document is the order they go in and why. One claim is worth stating before the first Recipe, because it is what the @@ -236,7 +236,7 @@ It is worth being clear about why this is its own plugin rather than part of what comes next. Joining a day's release notes, log lines or notifications into one document is the same operation, and it is useful with no AI service anywhere in the Recipe. The plugin adds no prompt and knows nothing about what -reads its result; what the joined text is *for* is decided entirely by the +reads its result. What the joined text is *for* is decided entirely by the plugin the Recipe puts after it. Note also what the joined item lost: its link. This is the stage the ordering @@ -371,12 +371,12 @@ pipeline the two arrangements differ: | AI filter → `FilterJoin` | 3 | Three separate texts, joined afterwards | Both are legitimate, and they answer different questions. A digest that draws a -theme across the day's articles needs the first; a document of per-article +theme across the day's articles needs the first, and a document of per-article summaries needs the second. The second is also the one that grows with the feed: a run that finds forty new articles makes forty requests, each of them billed and rate-limited. `StorePermalink` earlier in the Recipe is the main defence, -because it means each article is sent once ever; `FilterOne` ahead of the filter -is the blunt one, while a Recipe is being written. +because it means each article is sent once ever. `FilterOne` ahead of the +filter is the blunt one, while a Recipe is being written. The point is not which order is better. It is that the difference between "one digest" and "a list of summaries" is the position of one entry in a YAML list, @@ -386,8 +386,9 @@ and that no plugin had to be changed or configured to express it. Automatic Ruby uses separate service-specific AI filters rather than one plugin with a `provider` setting. Their current catalogue and settings are -maintained in [`PLUGINS.md`](PLUGINS.md) section 6.3. Each replaces an item's description with what its service answers, so -in a Recipe they are interchangeable at the same position: +maintained in [`PLUGINS.md`](PLUGINS.md) section 6.3. Each replaces an item's +description with what its service answers, so in a Recipe they are +interchangeable at the same position: ```yaml - module: FilterOpenAI @@ -435,9 +436,10 @@ in a Recipe they are interchangeable at the same position: ``` Nothing before or after the swapped entry changes. `token`, `model` and -`prompt` are shared required settings of the AI filters; `max_tokens` exists -only for Claude because that API requires it. Model names move with the services, so take them from the provider -you are using rather than from this document, and see +`prompt` are shared required settings of the AI filters. `max_tokens` exists +only for Claude because that API requires it. Model names move with the +services, so take them from the provider you are using rather than from this +document, and see [`PLUGINS.md`](PLUGINS.md) section 6.3 for each plugin's endpoint, authentication and answer handling. @@ -510,10 +512,11 @@ What that buys, concretely: articles, section 3 publishes a joined document, section 4 publishes an answer. When the last stage fails, the earlier ones still say what the run found. -- **A failure has one address.** Nothing was fetched is `CustomFeedWeb`; - everything is a summary is `FilterFullFeed` and its siteinfo; the document is - one section is `FilterJoin`; the request was refused is the AI filter. A - single plugin that did all of it would have one log and one place to guess in. +- **A failure has one address.** When nothing was fetched, it is + `CustomFeedWeb`. When everything is a summary, it is `FilterFullFeed` and its + siteinfo. When the document is one section, it is `FilterJoin`. When the + request was refused, it is the AI filter. A single plugin that did all of it + would have one log and one place to guess in. - **Changing behaviour is editing configuration, not code.** A different service, a different job, one digest instead of many summaries: three edits to one YAML file, each of them one line or one setting. diff --git a/doc/BASIC_DESIGN.md b/doc/BASIC_DESIGN.md index c187988..95cb0ee 100644 --- a/doc/BASIC_DESIGN.md +++ b/doc/BASIC_DESIGN.md @@ -9,8 +9,8 @@ travels through a run. What the system is for belongs to [`REQUIREMENTS.md`](REQUIREMENTS.md). The rules a change is held to belong to [`POLICY.md`](POLICY.md). The two public contracts — the Recipe format and the plugin interface — are specified in -[`PLUGINS.md`](PLUGINS.md); this document explains the machinery that implements -them and does not restate them. +[`PLUGINS.md`](PLUGINS.md). This document explains the machinery that +implements them and does not restate them. It stands on its own. Nothing in it is completed by a document kept in another repository. @@ -36,7 +36,7 @@ equally permanent. requires its own libraries, at the top of its own file, so that an absent gem is an error only for a Recipe that asked for that plugin. - **The library never exits and never prints.** Exit status is decided by the - entry point; user-facing text is written by the entry point or logged. + entry point, and user-facing text is written by the entry point or logged. Maintenance strength follows the architectural layer: @@ -183,8 +183,8 @@ Turns a Recipe file into an object the pipeline can iterate. - Parses the YAML **safely** — the document may contain only the plain types a Recipe needs, so a Recipe cannot name a Ruby class to instantiate. Aliases are permitted, because they are a legitimate way to share a block of settings - between plugins. This is a second line of defence, not the trust boundary; see - [`REQUIREMENTS.md`](REQUIREMENTS.md) section 17. + between plugins. This is a second line of defence, not the trust boundary. + See [`REQUIREMENTS.md`](REQUIREMENTS.md) section 17. - Wraps the result in `Hashie::Mash`, which is why a plugin entry answers to `plugin.module` and `plugin.config`, and why an absent setting reads as `nil` rather than raising. @@ -242,7 +242,7 @@ A `Logger` on standard output behind a level filter. - `Log.level(name)` sets the threshold, from `global.log.level`. - `Log.puts(level, message)` emits when `level` is at least the threshold. - Levels are `info`, `warn`, `error`, `none`, compared by their position in that - list; `none` is the threshold that admits nothing. + list. `none` is the threshold that admits nothing. - Both a `String` and a `Symbol` are accepted for a level, because plugins have always passed both, and an unknown level is treated as `info` rather than raising in the middle of an unattended run. @@ -271,7 +271,7 @@ value has the shape it has. - `Http.read(url)` fetches a URL and returns the body and `Http.open(url)` yields the stream, for a caller — an HTML parser, which detects a page's - encoding for itself — that would rather not be handed a decoded string; + encoding for itself — that would rather not be handed a decoded string. `Http.uri(url)` returns a validated URI and `Http.fetchable?(url)` answers whether there is one. @@ -288,7 +288,7 @@ article. Every plugin is a class in `Automatic::Plugin`, constructed with `(config, pipeline)` and answering `run`. The contract is specified in -[`PLUGINS.md`](PLUGINS.md) section 3; what matters to this document is how the +[`PLUGINS.md`](PLUGINS.md) section 3. What matters to this document is how the categories divide responsibility: | Category | Directory | Receives | Returns | Role | @@ -307,7 +307,7 @@ that a `Filter` does not reach the network. Replaceability is part of this boundary. A plugin is not given the same permanence as the Recipe format or the plugin contract itself. The framework -preserves the rules that let plugins compose; it does not preserve a plugin +preserves the rules that let plugins compose. It does not preserve a plugin whose external purpose has disappeared, and it does not move a plugin's domain behaviour into the core merely to make that behaviour permanent. @@ -337,7 +337,7 @@ or to a file. What matters to this document is where it is *not*: framework file mentions the format. A reference to it in `lib/` would be the same design error as a reference to any other plugin (section 3). - **Not a second pipeline value.** Markdown is produced *from* the pipeline at - the moment the pipeline ends; it is never passed along one. The plugin returns + the moment the pipeline ends. It is never passed along one. The plugin returns its input, so a plugin placed after it receives exactly what it would have received without it, and Invariant 2 of [`POLICY.md`](POLICY.md) is untouched. - **Not implicit.** Nothing appends it to a Recipe. `Pipeline.run` runs the @@ -519,8 +519,9 @@ be the framework acquiring a plugin's problem. It is resolved where the choice already exists — in the Recipe: - `global.log.level: none` silences the log for that run, leaving standard - output carrying the document alone; -- or the plugin writes to a file it is given, and the log keeps standard output. + output carrying the document alone. +- Alternatively, the plugin writes to a file it is given, and the log keeps + standard output. Both are stated in [`PLUGINS.md`](PLUGINS.md) section 6.7 and in [`DEPLOYMENT.md`](DEPLOYMENT.md), which is where an operator looks. diff --git a/doc/DEPLOYMENT.md b/doc/DEPLOYMENT.md index 28cb278..ce60510 100644 --- a/doc/DEPLOYMENT.md +++ b/doc/DEPLOYMENT.md @@ -3,9 +3,9 @@ How to install Automatic Ruby, set up a user directory, write and verify a Recipe, put it in `cron`, and operate it afterwards. -What the system is belongs to [`REQUIREMENTS.md`](REQUIREMENTS.md); how a Recipe -is written belongs to [`PLUGINS.md`](PLUGINS.md). This document assumes you have -read neither and points at them where they are needed. +What the system is belongs to [`REQUIREMENTS.md`](REQUIREMENTS.md), and how a +Recipe is written belongs to [`PLUGINS.md`](PLUGINS.md). This document assumes +you have read neither and points at them where they are needed. It stands on its own. Nothing in it is completed by a document kept in another repository. @@ -26,22 +26,22 @@ nothing to stop. ## Before you begin -- A Unix-like system. GNU/Linux and macOS are what this is used on; Windows is +- A Unix-like system. GNU/Linux and macOS are what this is used on. Windows is not supported. - **Ruby 3.3 through 4.0.** Check with `ruby -v`. The continuously validated - versions are the matrix in [`.github/workflows/ci.yml`](../.github/workflows/ci.yml); - a supported version absent from that matrix is simply not checked on every + versions are the matrix in [`.github/workflows/ci.yml`](../.github/workflows/ci.yml). + A supported version absent from that matrix is simply not checked on every commit, and a Ruby newer than the matrix is permitted rather than refused. See [`REQUIREMENTS.md`](REQUIREMENTS.md) section 20. - Optional gems for particular plugins, listed in the table under "Optional plugin dependencies" below. None is needed to install Automatic - Ruby, or to run a Recipe that does not use the plugin; a Recipe that does + Ruby, or to run a Recipe that does not use the plugin. A Recipe that does name one installs it as a step of its own, which is what the "Install what the Recipe needs" section of [`QUICKSTART.md`](QUICKSTART.md) demonstrates. - A build environment may be needed for one of those optional gems — `nokogiri` and `sqlite3` build a native extension where no binary package matches your platform. The framework's own dependencies are pure Ruby, so the normal - installation needs no build tools; install them only if installing an + installation needs no build tools. Install them only if installing an optional gem reports that they are required. ## Install @@ -55,9 +55,10 @@ automatic --version That installs the framework, the `automatic` command and the runtime dependencies declared in `automatic.gemspec`. That gemspec is the source of -truth for the framework's runtime dependency set. No HTML parser, no database, no service client — a gem needed by -one plugin is installed by the operator who uses that plugin, so installing -Automatic Ruby does not install what your Recipes do not use. +truth for the framework's runtime dependency set. No HTML parser, no database, +no service client — a gem needed by one plugin is installed by the operator who +uses that plugin, so installing Automatic Ruby does not install what your +Recipes do not use. Add one when you use the plugin that needs it: @@ -72,7 +73,7 @@ which, and is the list to check before adding anything. ### From a checkout For working on the framework, or for running a version that is not released. -The supported checkout setups are shown below; start with the minimal setup. +The supported checkout setups are shown below. Start with the minimal setup. **Minimal — the framework and its test suite.** What you want for running the checkout, and for developing the framework itself: @@ -104,7 +105,7 @@ That installs the gems declared in the `plugins` group of `Gemfile`, and their specs then run as part of the ordinary suite. The [Optional plugin dependencies](#optional-plugin-dependencies) table maps those dependencies to the plugins and tools that use them. The setting -is written to the checkout's own `.bundle/config`, which is not committed; +is written to the checkout's own `.bundle/config`, which is not committed. `bundle config unset --local with` returns the checkout to the minimum, and `bundle install` afterwards. @@ -118,8 +119,8 @@ bundle install ``` Several at once are space-separated: `bundle config set --local with "store -html"`, and a Recipe using plugins from multiple groups needs exactly that. The group -names are in the table below, and "Working out what a Recipe needs, in a +html"`, and a Recipe using plugins from multiple groups needs exactly that. The +group names are in the table below, and "Working out what a Recipe needs, in a checkout" takes one Recipe through choosing them. In a checkout, `gem install ` on its own is **not** enough: the checkout @@ -172,7 +173,7 @@ from an optional-dependency failure: particular feed, not the installation. - A `LoadError` naming a gem means a plugin's optional dependency is not installed. `feed2console.yml` uses none, so at this point it means the - installation itself; a message naming a gem and a plugin means the plugin, + installation itself. A message naming a gem and a plugin means the plugin, and "Optional plugin dependencies" below says what to install. To check the framework without any network at all, write a Recipe that uses @@ -298,7 +299,7 @@ unattended is below. **The output path.** `file` is expanded, so `~` works, and a relative path is resolved against the process's working directory — which under `cron` is your home directory and is not worth relying on. Use an absolute path or a `~` path. -A missing parent directory is created; nothing else on the path is touched, and +A missing parent directory is created. Nothing else on the path is touched, and the plugin writes to no other file. **Append or overwrite.** `mode: append`, the default, adds the run's items to @@ -321,7 +322,8 @@ mkdir -p ~/notes && chmod 700 ~/notes **Keeping the document clean.** With no `file`, the document goes to standard output — and so does the log ([`REQUIREMENTS.md`](REQUIREMENTS.md) section 14), -so a redirect that collects one collects the other. Either disable log output in the Recipe or give `PublishMarkdown` a `file`: +so a redirect that collects one collects the other. Either disable log output +in the Recipe or give `PublishMarkdown` a `file`: ```yaml global: @@ -348,8 +350,8 @@ needs somewhere else to go: 0 7 * * * /usr/local/bin/automatic -c $HOME/.automatic/config/feeds.yml >> $HOME/notes/feeds.md 2>>$HOME/.automatic/log/feeds.log ``` -That entry is only clean if the Recipe sets `log.level: none`; otherwise the log -lines land in the document. Setting the level in the Recipe is the supported +That entry is only clean if the Recipe sets `log.level: none`. Otherwise, the +log lines land in the document. Setting the level in the Recipe is the supported way to do this, and the framework has no separate switch for it. **Afterwards.** The file is ordinary text: read it, `grep` it, feed it to @@ -375,7 +377,7 @@ Rules worth holding to: credential in a log, that is a defect worth reporting. This is a weakness inherited from the original design rather than a decision -made now; it is recorded in [`REQUIREMENTS.md`](REQUIREMENTS.md) section 17. +made now. It is recorded in [`REQUIREMENTS.md`](REQUIREMENTS.md) section 17. ## Schedule it @@ -430,7 +432,7 @@ global: - `none` — nothing. Once a Recipe is settled, `warn` keeps the log to what needs reading. Rotate the -file yourself; the framework does not. +file yourself, because the framework does not. ## Upgrading @@ -480,10 +482,10 @@ bundle. `plugins` is every group in the first block at once. Membership of the aggregate `plugins` group is defined in `Gemfile`. It contains the optional gems whose corresponding specs need no external -service; the table above maps those dependencies to their consumers. The gems below it are in their own groups -only, because each of those plugins also needs a service, a bucket or a -command, and installing a gem alone would not make the plugin — or its spec — -work. +service. The table above maps those dependencies to their consumers. The gems +below it are in their own groups only, because each of those plugins also needs +a service, a bucket or a command, and installing a gem alone would not make the +plugin — or its spec — work. Both S3 rows make their requirement lazily, so `StoreFile`'s ordinary HTTP download path and `PublishAmazonS3` in `mode: test` work with no AWS gem @@ -677,9 +679,9 @@ prefix runs the specs, the diagnostic subcommands and the Recipe, and typing **What "is not installed" means in that message.** It is the framework reporting that **this process could not `require` the library**, which is wider than "no copy of the gem exists on this machine". A gem installed by `gem -install` but outside the checkout's bundle produces it; so does a group selected +install` but outside the checkout's bundle produces it. So does a group selected but not installed, and so does an installation under a different Ruby. The gem -name and the plugin name in the message are what to act on; whether to act with +name and the plugin name in the message are what to act on. Whether to act with a group or with `gem install` is decided by where you are running from, not by the message. @@ -723,7 +725,7 @@ development and for a checkout you are developing in. Naming the groups is the right setting for **running** Recipes, and is what the design of the split asks for: a gem is installed by the operator who uses the plugin that needs it, and a checkout that runs this Recipe has no reason to -build `sanitize` or to hold `feedbag`. Start there; `plugins` is not a shortcut +build `sanitize` or to hold `feedbag`. Start there. `plugins` is not a shortcut for having read the table, and a checkout that has installed everything hides the group a Recipe of yours will need on the next machine. @@ -743,8 +745,8 @@ bundle config unset --local with bundle install ``` -The gems stay on the machine; what changes is that the bundle no longer includes -them, and the plugins that need them report them as missing again. +The gems stay on the machine. What changes is that the bundle no longer +includes them, and the plugins that need them report them as missing again. ## Your own plugins @@ -757,7 +759,7 @@ installation is the intended use. $EDITOR ~/.automatic/plugins/filter/my_filter.rb ``` -The class is `Automatic::Plugin::FilterMyFilter`; the contract and a worked +The class is `Automatic::Plugin::FilterMyFilter`. The contract and a worked example are in [`PLUGINS.md`](PLUGINS.md) sections 3 and 4. A gem upgrade does not touch this directory. @@ -770,12 +772,12 @@ not touch this directory. dependency could not be required. The message names the gem, the plugin and the command to install it, and the table above says the same thing. In a checkout it also means a gem installed outside the bundle, or a group selected but not -installed; "Why `bundle exec`, and why `gem install` is not enough here" above +installed. "Why `bundle exec`, and why `gem install` is not enough here" above tells the cases apart. **`Automatic::NoPluginError: unknown plugin named ...`** — a Recipe names a plugin that does not ship. Check the spelling against -[`PLUGINS.md`](PLUGINS.md) section 6; if the name is in section 8, the plugin +[`PLUGINS.md`](PLUGINS.md) section 6. If the name is in section 8, the plugin was removed because the service behind it no longer exists, and the Recipe needs a different last step rather than a reinstall. Nothing has run when this is raised. @@ -789,7 +791,7 @@ file in the wrong category directory is never found. `~/.automatic/config` only. Use a path with a `/` in it for anything else. **The run stops partway** — a plugin raised, and the framework does not catch -plugin exceptions. Everything before it has already happened; there is no +plugin exceptions. Everything before it has already happened, and there is no rollback. Rerun after fixing the cause. If the plugins after the failure must not repeat their effect, that is what a store plugin in front of them is for. diff --git a/doc/PLUGINS.md b/doc/PLUGINS.md index bbc46a5..efce9f8 100644 --- a/doc/PLUGINS.md +++ b/doc/PLUGINS.md @@ -8,8 +8,8 @@ contract**, which plugin authors implement. It also catalogues the plugins shipped in the gem, with what each one accepts and whether it still works. The system these two interfaces belong to is described in -[`REQUIREMENTS.md`](REQUIREMENTS.md); the machinery that implements them is in -[`BASIC_DESIGN.md`](BASIC_DESIGN.md); the rules for changing them are in +[`REQUIREMENTS.md`](REQUIREMENTS.md). The machinery that implements them is in +[`BASIC_DESIGN.md`](BASIC_DESIGN.md), and the rules for changing them are in [`POLICY.md`](POLICY.md). It stands on its own. Nothing in it is completed by a document kept in another @@ -276,8 +276,8 @@ plugin's behaviour without editing the installation. Creating a new category is creating a directory. `~/.automatic/plugins/mine/` plus a class named `MineSomething` works with no change to the framework, though -staying inside the established categories listed in section 3.2 is preferred, because their names tell a -reader where in a pipeline the plugin belongs. +staying inside the established categories listed in section 3.2 is preferred, +because their names tell a reader where in a pipeline the plugin belongs. Loading is lazy: the loader registers an `autoload`, so the file is read when the constant is first used. A syntax error in a plugin therefore surfaces when @@ -315,8 +315,8 @@ Rules that follow from the shape: items ends the pipeline for everything after it. - **`link` may be `nil`, and so may any other field.** A missing field is data, not a framework-wide drop signal: `FeedMaker.create_pipeline` preserves an - item whose link is `nil`. A plugin that requires a link must check it itself; - a plugin that does not may keep processing the item. + item whose link is `nil`. A plugin that requires a link must check it itself. + A plugin that does not may keep processing the item. - **Guard the feed itself.** `@pipeline.each { |feeds| next if feeds.nil? }` is the prevailing idiom, because a subscription plugin that failed may have put a `nil` in the array. @@ -333,7 +333,7 @@ Rules that follow from the shape: - Assume nothing. `@config` itself may be `nil`, and any key may be missing. - Follow the established names: `retry` for an attempt count, `interval` for seconds between attempts, `db` for a database file, `path` for a directory. -- Do not choose a setting name that is a method of `Hash` or `Enumerable`; see +- Do not choose a setting name that is a method of `Hash` or `Enumerable`. See section 2.6.1. - Do not read the environment, and do not read a file other than one named in the settings. The `config` mapping is the plugin's entire input besides the @@ -358,9 +358,9 @@ end - **A failure is logged.** Whatever the plugin decides to do about an error, the log is the only record an unattended run leaves. Swallowing an error silently - is a defect; so is logging it at `info`. + is a defect, and so is logging it at `info`. - **Raising is allowed, and it ends the run.** The framework does not catch - plugin exceptions. Raise when continuing would be wrong; rescue when the + plugin exceptions. Raise when continuing would be wrong, and rescue when the Recipe should carry on with less data. - **Log through `Automatic::Log`, not `puts`.** The exception is a plugin whose purpose is to write to the terminal, which holds an output object in an @@ -371,16 +371,16 @@ end Credentials arrive as ordinary settings, which makes the Recipe holding them a secret file. A plugin therefore: -- never logs a credential, and never logs `@config` wholesale; -- never writes one into a pipeline item, where a later publishing plugin would - send it somewhere; -- verifies TLS certificates. Disabling verification is not acceptable, whatever +- Never logs a credential, and never logs `@config` wholesale. +- Never writes one into a pipeline item, where a later publishing plugin would + send it somewhere. +- Verifies TLS certificates. Disabling verification is not acceptable, whatever a service's certificate is doing. ### 3.8 Dependencies A plugin requires its own libraries at the top of its own file. A library that -ships with Ruby is required plainly; a gem the operator has to install is +ships with Ruby is required plainly. A gem the operator has to install is required through `Automatic.require_optional`, which names the gem, the plugin and the way to install it if it is absent: @@ -401,7 +401,7 @@ Pass `gem_name:` where the gem's name differs from the path required, as That is what keeps a gem needed by one plugin out of everyone else's installation. A gem used by a single plugin is not added to the framework's -runtime dependencies; it goes in an optional group of the `Gemfile` and the +runtime dependencies. It goes in an optional group of the `Gemfile` and the operator who uses the plugin installs it. See [`POLICY.md`](POLICY.md) section 9. @@ -433,7 +433,7 @@ charset comes back tagged UTF-8 because that is the fallback, not because the page said so. A plugin that hands the body to an HTML parser wants `open` instead, because a parser given the stream reads the `meta` charset for itself and a parser given the string believes the tag. `FilterFullFeed` is the worked -example; the difference there was a whole article in mojibake. +example. The difference there was a whole article in mojibake. It is a helper and not a client: it opens the URL through `open-uri` with the scheme restricted to HTTP and HTTPS, a connect and a read timeout, a bounded @@ -466,7 +466,7 @@ describe Automatic::Plugin::FilterExample do end ``` -`AutomaticSpec.generate_pipeline` builds a pipeline; `feed` opens a feed object +`AutomaticSpec.generate_pipeline` builds a pipeline. `feed` opens a feed object and `item url, title, description, date, author, source, enclosure` adds one item to it. @@ -560,7 +560,7 @@ Section 6 lists every plugin shipped in the gem. Each carries a status: | **Needs rework** | The service and the capability still exist, but this plugin speaks an interface that has been replaced. It will not work as written, and restoring it is a self-contained piece of work. | No status is used for an integration whose service or capability is -permanently unavailable. Such plugins are removed rather than retained; see +permanently unavailable. Such plugins are removed rather than retained. See section 8. The rules that govern this table are the reason it exists at all: @@ -569,7 +569,7 @@ The rules that govern this table are the reason it exists at all: test pass or a catalogue entry look better. Where a plugin's gem is absent its specs are excluded from the default suite, and that absence is the honest signal. A service that no longer answers is not given a fake endpoint to - answer with; the plugin goes. + answer with. The plugin goes. - **Nothing is kept for being old.** A plugin ships because it has a current practical use, not because it once did. Git history is where the previous implementations are, and it keeps them without their being installed on @@ -581,14 +581,15 @@ that read HTML, `FilterSanitize`, `FilterDescriptionLink` — works, and is simply not part of what a green required build guarantees, because the default bundle does not install that gem. Its entry says so, and installing the gem runs its spec as part of the ordinary suite, which is also what the separate -`plugins` workflow does. Nothing here is classified by what CI happens to run; a +`plugins` workflow does. Nothing here is classified by what CI happens to run. A plugin is not demoted for needing a gem, and is not promoted by a test that CI never executes. **Statuses involving an outside service are snapshots of external state.** -They can change without any commit here. Where a status was reached from published information rather than from a -live check, the entry says so. To verify one yourself, run its Recipe from -`test/integration` by hand; those are not part of CI and never will be. +They can change without any commit here. Where a status was reached from +published information rather than from a live check, the entry says so. To +verify one yourself, run its Recipe from `test/integration` by hand. Those are +not part of CI and never will be. Restoring a plugin classified as **Needs rework** is a self-contained piece of work and a good first contribution. @@ -599,7 +600,7 @@ of work and a good first contribution. ### 6.1 Subscription -Acquire from outside; produce a pipeline. Called first in a Recipe. +Acquire from outside, and produce a pipeline. Called first in a Recipe. #### SubscriptionFeed — **Supported** @@ -622,7 +623,7 @@ with. | `retry` | integer | Attempts after the first, per feed. Default `0`. | | `interval` | integer | Seconds between attempts. Default `0`. | -A feed that fails after its retries is logged and skipped; the others still run. +A feed that fails after its retries is logged and skipped. The others still run. `interval` is now waited. The line that was meant to wait it assigned to a local variable named `sleep` and returned at once, in this plugin and in every @@ -669,10 +670,10 @@ no network, which makes it the plugin to test a Recipe's later half with. | `titles` | sequence | One item per title, no link | | `urls` | sequence | One item per URL, no title | | `feeds` | sequence | Mappings of `title`, `url`, `description`, `author`, `comments` | -| `files` | sequence | UTF-8 TSV paths; positional columns are title, url, description, author, comments | +| `files` | sequence | UTF-8 TSV paths. Positional columns are title, url, description, author, comments | The TSV separator is a tab and `~` is expanded. Empty columns supply no value -for that field without shifting later columns; blank or all-empty rows produce +for that field without shifting later columns. Blank or all-empty rows produce no item. Only the first five columns are used. Line endings are removed before splitting, but other field whitespace is preserved. Any combination of the four keys may be given. @@ -745,9 +746,9 @@ kept working forever is a second format to support. | `description_selector` | string | The summary the page prints, taken as text. | | `date_selector` | string | The publication date, inside the article. | | `same_host` | boolean | Drop a URL whose host is not the page's. Default `true`. | -| `include` | sequence | Regular expressions; a URL matching none of them is dropped. | -| `exclude` | sequence | Regular expressions; a URL matching one of them is dropped. | -| `fetch_items` | integer | Items per page, from the top. Default `100`; `0`, a negative value and an absent one all mean the default. | +| `include` | sequence | Regular expressions. A URL matching none of them is dropped. | +| `exclude` | sequence | Regular expressions. A URL matching one of them is dropped. | +| `fetch_items` | integer | Items per page, from the top. Default `100`. `0`, a negative value and an absent one all mean the default. | There are three ways a page is read, and which one applies follows from the selectors given: @@ -759,7 +760,7 @@ selectors given: - **`item_selector`.** Each node it selects is one article, and `link_selector`, `title_selector`, `description_selector` and `date_selector` are evaluated inside that node. Without `link_selector` the - article's first `a[href]` is the permalink; without `title_selector` the + article's first `a[href]` is the permalink. Without `title_selector` the link's own text is the title. `title_selector`, `description_selector` and `date_selector` are read inside an @@ -772,11 +773,11 @@ then judged in this order: HTTP or HTTPS, not the page itself, `same_host`, `include`, `exclude`, already seen, and finally `fetch_items`. The fragment is removed, because two links differing only in their anchor are one article. The query string is kept, because `?id=42` is frequently the whole of what -identifies one; no canonical form is guessed. `same_host` is an exact host +identifies one. No canonical form is guessed. `same_host` is an exact host match, so `blog.example.com` is not `www.example.com`. The page's own order is kept. A list page's order is the only ordering -information it carries, and nothing here sorts by date; `FilterSort` is where +information it carries, and nothing here sorts by date. `FilterSort` is where a Recipe asks for that. `date_selector` prefers the `datetime` attribute of a `