metanorma-release 0.2.2 → 0.2.3

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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +19 -1
  3. data/.rubocop_todo.yml +250 -319
  4. data/README.adoc +120 -233
  5. data/Rakefile +2 -2
  6. data/exe/metanorma-release +2 -2
  7. data/lib/metanorma/release/aggregation_pipeline.rb +59 -45
  8. data/lib/metanorma/release/asset_processor.rb +10 -8
  9. data/lib/metanorma/release/cache_store.rb +6 -6
  10. data/lib/metanorma/release/change_detector.rb +7 -3
  11. data/lib/metanorma/release/channel.rb +13 -39
  12. data/lib/metanorma/release/channel_filter.rb +26 -10
  13. data/lib/metanorma/release/cli.rb +129 -100
  14. data/lib/metanorma/release/commands/aggregate.rb +39 -54
  15. data/lib/metanorma/release/commands/package.rb +20 -12
  16. data/lib/metanorma/release/commands/{publish.rb → release_command.rb} +20 -12
  17. data/lib/metanorma/release/config.rb +104 -0
  18. data/lib/metanorma/release/content_hash.rb +11 -3
  19. data/lib/metanorma/release/delta_state.rb +55 -18
  20. data/lib/metanorma/release/file_routing.rb +8 -5
  21. data/lib/metanorma/release/index.rb +132 -0
  22. data/lib/metanorma/release/interfaces.rb +15 -15
  23. data/lib/metanorma/release/platform/github/manifest_reader.rb +4 -4
  24. data/lib/metanorma/release/platform/github/publisher.rb +23 -11
  25. data/lib/metanorma/release/platform/github/release_fetcher.rb +12 -3
  26. data/lib/metanorma/release/platform/github.rb +10 -7
  27. data/lib/metanorma/release/platform/local/directory_discoverer.rb +1 -1
  28. data/lib/metanorma/release/platform/local/fetcher.rb +17 -12
  29. data/lib/metanorma/release/platform/local/publisher.rb +9 -7
  30. data/lib/metanorma/release/platform/local.rb +4 -4
  31. data/lib/metanorma/release/platform/null/publisher.rb +3 -2
  32. data/lib/metanorma/release/platform/null.rb +1 -1
  33. data/lib/metanorma/release/platform.rb +3 -3
  34. data/lib/metanorma/release/platform_factory.rb +48 -29
  35. data/lib/metanorma/release/publication.rb +335 -0
  36. data/lib/metanorma/release/release_pipeline.rb +85 -52
  37. data/lib/metanorma/release/repo_ref.rb +5 -2
  38. data/lib/metanorma/release/site.rb +66 -0
  39. data/lib/metanorma/release/slug_strategy.rb +163 -0
  40. data/lib/metanorma/release/version.rb +1 -1
  41. data/lib/metanorma/release/zip_packager.rb +31 -8
  42. data/lib/metanorma/release.rb +68 -94
  43. metadata +22 -26
  44. data/lib/metanorma/release/aggregation_interfaces.rb +0 -27
  45. data/lib/metanorma/release/channel_audience.rb +0 -24
  46. data/lib/metanorma/release/channel_config.rb +0 -55
  47. data/lib/metanorma/release/channel_manifest.rb +0 -192
  48. data/lib/metanorma/release/channel_registry.rb +0 -60
  49. data/lib/metanorma/release/config_fetcher.rb +0 -11
  50. data/lib/metanorma/release/config_locator.rb +0 -37
  51. data/lib/metanorma/release/config_resolver.rb +0 -37
  52. data/lib/metanorma/release/document_id.rb +0 -45
  53. data/lib/metanorma/release/document_index.rb +0 -183
  54. data/lib/metanorma/release/document_metadata.rb +0 -39
  55. data/lib/metanorma/release/document_stage.rb +0 -86
  56. data/lib/metanorma/release/document_type.rb +0 -55
  57. data/lib/metanorma/release/document_version.rb +0 -50
  58. data/lib/metanorma/release/naming_strategy.rb +0 -158
  59. data/lib/metanorma/release/platform/github/config_fetcher.rb +0 -40
  60. data/lib/metanorma/release/platform/local/config_fetcher.rb +0 -20
  61. data/lib/metanorma/release/rake_tasks.rb +0 -71
  62. data/lib/metanorma/release/relaton_enricher.rb +0 -138
  63. data/lib/metanorma/release/release_metadata.rb +0 -79
  64. data/lib/metanorma/release/release_tag.rb +0 -49
  65. data/lib/metanorma/release/rxl_extractor.rb +0 -115
  66. data/lib/metanorma/release/stage_filter.rb +0 -18
data/README.adoc CHANGED
@@ -8,13 +8,16 @@ toc::[]
8
8
 
9
9
  == Overview
10
10
 
11
- `metanorma-release` manages the full release lifecycle of Metanorma documents:
11
+ `metanorma-release` manages the full release lifecycle of Metanorma documents through three actors:
12
12
 
13
- **Release** (producer side)::
14
- Discover compiled documents -> extract metadata from RXL -> detect changes -> package as zip -> publish to a platform (GitHub Releases, local filesystem).
13
+ **Doc repo** (producer)::
14
+ Discover compiled documents -> extract metadata from RXL -> detect changes -> package as zip -> release to a platform (GitHub Releases, local filesystem).
15
15
 
16
- **Aggregate** (consumer side)::
17
- Discover repositories -> fetch published releases -> filter by channel and stage -> extract zip assets -> generate `index.json` with a file tree for any site generator.
16
+ **Org/publisher** (governor)::
17
+ Define channels and routing rules via config. Map document metadata (stage, doctype) to channel labels that control visibility and distribution.
18
+
19
+ **Aggregator** (consumer)::
20
+ Discover repositories -> fetch published releases -> filter by channel and stage -> extract zip assets -> generate `index.json` with Relaton enrichment and a file tree for any site generator.
18
21
 
19
22
  The output is platform-agnostic: a directory containing `index.json` and a tree of document files. Any site generator (Jekyll, Hugo, Vite) consumes that output independently.
20
23
 
@@ -34,7 +37,11 @@ Or install directly:
34
37
  gem install metanorma-release
35
38
  ----
36
39
 
37
- Requires Ruby >= 3.2.
40
+ Requires Ruby >= 3.2. Optional runtime dependencies:
41
+
42
+ * `relaton-bib` -- RXL metadata extraction (required for `package` and `release`)
43
+ * `octokit` -- GitHub platform adapter (required for GitHub releases/aggregation)
44
+ * `rubyzip` -- zip packaging (required for `package` and `release`)
38
45
 
39
46
  == Quick start
40
47
 
@@ -45,67 +52,39 @@ The gem ships three commands:
45
52
  [source,sh]
46
53
  ----
47
54
  # Package compiled documents as zip archives
48
- metanorma-release package --output-dir _site --manifest metanorma.release.yml
55
+ metanorma-release package --output-dir _site
49
56
 
50
- # Package and publish to a platform
51
- metanorma-release publish --platform github --output-dir _site --token $GITHUB_TOKEN
57
+ # Package and release to a platform
58
+ metanorma-release release --platform github --output-dir _site --token $GITHUB_TOKEN
52
59
 
53
60
  # Aggregate published releases into a file tree + index.json
54
- metanorma-release aggregate --source github --organizations my-org --output-dir _site/cc
55
- ----
56
-
57
- === Rake tasks
58
-
59
- Register tasks in your `Rakefile`:
60
-
61
- [source,ruby]
62
- ----
63
- require "metanorma/release/rake_tasks"
64
-
65
- Metanorma::Release::RakeTasks.install do |config|
66
- config.output_dir = "_site"
67
- config.manifest = "metanorma.release.yml"
68
- config.platform = "github"
69
- end
61
+ metanorma-release aggregate --repos my-org/my-repo --output-dir _site/cc
70
62
  ----
71
63
 
72
- This provides:
73
-
74
- * `rake mn:package` -- package compiled documents
75
- * `rake mn:publish` -- package and publish documents
76
- * `rake mn:aggregate` -- aggregate published releases
77
-
78
64
  === Ruby API
79
65
 
80
- Use the pipelines directly for fine-grained control:
81
-
82
66
  [source,ruby]
83
67
  ----
84
- deps = Metanorma::Release::ReleasePipeline::Dependencies.new(
85
- extractor: Metanorma::Release::RxlExtractor.new,
86
- filters: [],
87
- change_detector: Metanorma::Release::ContentHashChangeDetector.new(previous_releases: {}),
88
- packager: Metanorma::Release::ZipPackager.new,
89
- publisher: Metanorma::Release::PlatformFactory.build_publisher("null", {}),
90
- naming_registry: Metanorma::Release::NamingRegistry.default_registry,
91
- manifest: nil,
92
- channel_override: nil,
93
- channel_config: nil
94
- )
68
+ # Discover publications from compiled RXL files
69
+ publications = Metanorma::Release::Publication.discover("_site")
95
70
 
96
- config = Metanorma::Release::ReleasePipeline::Config.new(
97
- output_dir: "_site",
98
- manifest_path: nil,
99
- force: false,
100
- force_replace_patterns: nil,
101
- concurrency: 4,
102
- default_visibility: "public"
103
- )
71
+ # Each publication carries metadata from Relaton
72
+ pub = publications.first
73
+ pub.identifier # => "CC 18011:2018"
74
+ pub.slug # => "cc-18011-2018"
75
+ pub.title # => "Date and time — Explicit representation"
76
+ pub.edition # => "1"
77
+ pub.stage # => "60"
78
+ pub.doctype # => "standard"
79
+ pub.formats # => ["html", "pdf", "xml"]
104
80
 
105
- result = Metanorma::Release::ReleasePipeline.new(deps).run(config)
106
- result.released # => [#<DocumentMetadata ...>]
107
- result.skipped # => [#<DocumentMetadata ...>]
108
- result.failed # => [{ document: ..., error: "..." }]
81
+ # Serialization (used in release body and sidecar metadata)
82
+ pub.to_release_body # => "<!-- mn-release-metadata\n{...}\n-->"
83
+ pub.to_json # => "{...}"
84
+
85
+ # Parse from release body (used in aggregation)
86
+ pub = Publication.from_release_body(body)
87
+ pub = Publication.from_json(json_string)
109
88
  ----
110
89
 
111
90
  == CLI reference
@@ -125,30 +104,30 @@ metanorma-release package [options]
125
104
  |`--output-dir DIR` |Directory containing compiled documents (default: `_site`)
126
105
  |`--dest DIR` |Destination for zip packages (default: `dist`)
127
106
  |`--manifest FILE` |Release manifest file (default: `metanorma.release.yml`)
128
- |`--config SOURCE` |Channel config file or platform ref
107
+ |`--config SOURCE` |Config file
129
108
  |===
130
109
 
131
- === `metanorma-release publish`
110
+ === `metanorma-release release`
132
111
 
133
- Package and publish documents to a platform.
112
+ Package and release documents to a platform.
134
113
 
135
114
  [source,sh]
136
115
  ----
137
- metanorma-release publish [options]
116
+ metanorma-release release [options]
138
117
  ----
139
118
 
140
119
  [cols="1m,3",options="header"]
141
120
  |===
142
121
  |Option |Description
143
- |`--platform NAME` |Target platform: `github`, `local` (default: `github`)
122
+ |`--platform NAME` |Target platform: `github`, `local`, `null` (default: `github`)
144
123
  |`--output-dir DIR` |Compiled docs directory (default: `_site`)
145
124
  |`--manifest FILE` |Release manifest file (default: `metanorma.release.yml`)
146
125
  |`--force` |Force release even if unchanged
147
126
  |`--force-replace PAT` |Glob pattern for forced replacement (repeatable)
148
- |`--channels CHANS` |Override channels (comma-separated)
127
+ |`--channels CHANS` |Override channels
149
128
  |`--concurrency N` |Parallel workers (default: 4)
150
129
  |`--token TOKEN` |Platform auth token
151
- |`--config SOURCE` |Channel config file or platform ref
130
+ |`--config SOURCE` |Config file
152
131
  |===
153
132
 
154
133
  === `metanorma-release aggregate`
@@ -164,11 +143,11 @@ metanorma-release aggregate [options]
164
143
  |===
165
144
  |Option |Description
166
145
  |`--source SOURCE` |Discovery source: `github`, `local:PATH` (default: `github`)
167
- |`--organizations ORGS` |Comma-separated organization list
146
+ |`--organizations ORGS` |Organization list
168
147
  |`--topic TOPIC` |Repository topic filter (default: `metanorma-release`)
169
- |`--repos REPOS` |Explicit repo list (comma-separated)
170
- |`--channels CHANS` |Filter channels (comma-separated)
171
- |`--stages STAGES` |Filter stages (comma-separated)
148
+ |`--repos REPOS` |Explicit repo list
149
+ |`--channels CHANS` |Filter channels
150
+ |`--stages STAGES` |Filter stages
172
151
  |`--output-dir DIR` |Output directory (default: `_site/cc`)
173
152
  |`--file-routing MODE` |File layout: `by-document`, `flat`, `by-format` (default: `by-document`)
174
153
  |`--cache-dir DIR` |Cache directory for delta state
@@ -180,110 +159,79 @@ metanorma-release aggregate [options]
180
159
 
181
160
  == Concepts
182
161
 
183
- === Channels
162
+ === Publication
184
163
 
185
- A channel is an `audience/category` pair that controls who can access a document:
164
+ The central domain model. A `Publication` carries metadata from Relaton RXL extraction, files from the filesystem, and channels from config routing.
186
165
 
187
166
  [source,ruby]
188
167
  ----
189
- channel = Metanorma::Release::Channel.parse("public/standards")
190
- channel.public? # => true
191
- channel.audience # => "public"
192
- channel.category # => "standards"
193
- ----
168
+ pub = Metanorma::Release::Publication.new(
169
+ identifier: "CC 18011:2018",
170
+ slug: "cc-18011-2018",
171
+ title: "Date and time — Explicit representation",
172
+ edition: "1",
173
+ stage: "60",
174
+ doctype: "standard",
175
+ revdate: "2018-06-01",
176
+ files: [PublicationFile.new(format: "html", name: "cc-18011.html", path: "cc-18011.html")],
177
+ channels: ["public"]
178
+ )
194
179
 
195
- Audiences: `public`, `members`, `internal`. When omitted, audience defaults to `public`.
180
+ pub.base_dir # => "."
181
+ pub.content_hash # => #<ContentHash ...>
182
+ pub.with_channels(["members"]) # => new Publication with different channels
183
+ ----
196
184
 
197
- === Channel configuration
185
+ === Channels
198
186
 
199
- A channel config defines the set of allowed channels for a project or organization, along with default visibility. This lets you enforce a channel taxonomy across all documents.
187
+ Channels are simple string labels that control document visibility and distribution. Typical values: `public`, `members`, `internal`.
200
188
 
201
- .Config resolution order
202
- [arabic]
203
- . `--config` CLI flag (highest priority)
204
- . `config:` key in the release manifest
205
- . Directory walk: `.metanorma.yml`, `.metanorma.yaml`, or `.metanorma/channels.yml`
206
- . No config -- all channels allowed
189
+ === Config
207
190
 
208
- ==== Config file format
191
+ A `metanorma.release.yml` config file defines channels and routing rules for an organization:
209
192
 
210
193
  [source,yaml]
211
194
  ----
212
- # .metanorma.yml
213
195
  channels:
214
- - public/standards
215
- - public/reports
216
- - members/early-access
217
- - internal/working-drafts
218
- defaults:
219
- visibility: public
220
- channels:
221
- - public/standards
222
- ----
223
-
224
- The `channels` list defines the taxonomy -- only these channels are valid. The `defaults` section sets fallback visibility and channels when a document doesn't match any manifest entry.
196
+ - public
197
+ - members
198
+ - internal
225
199
 
226
- ==== Specifying config in the manifest
200
+ routing:
201
+ default: [public]
202
+ rules:
203
+ - stage: ["20", "30"]
204
+ channels: [internal]
205
+ - stage: ["60"]
206
+ channels: [public]
207
+ - doctype: [report]
208
+ channels: [public]
227
209
 
228
- Add a `config` key to `metanorma.release.yml`:
229
-
230
- [source,yaml]
210
+ slug:
211
+ default: edition
212
+ strategies:
213
+ ietf: internet-draft
214
+ ieee: draft-suffix
215
+ iho: version
216
+ ogc: version
231
217
  ----
232
- config: local:/path/to/config.yml
233
- defaults:
234
- visibility: public
235
- documents:
236
- - source: sources/cc-18011.adoc
237
- channels:
238
- - public/standards
239
- ----
240
-
241
- The config source can be:
242
218
 
243
- * `local:/path/to/config.yml` -- local file path
244
- * `myorg/myrepo` -- GitHub repo (reads `channels.yml` from root)
245
- * `myorg/myrepo#path/to/config.yml` -- GitHub repo with explicit path
219
+ Routing rules match raw metadata values from Relaton (stage, doctype) to channel labels. When no config is present, all documents route to `public`.
246
220
 
247
- ==== Ruby API
221
+ === Slug strategies
248
222
 
249
- [source,ruby]
250
- ----
251
- # Parse from YAML
252
- config = Metanorma::Release::ChannelConfig.from_yaml(File.read(".metanorma.yml"))
253
-
254
- # Permissive config (all channels allowed)
255
- config = Metanorma::Release::ChannelConfig.empty
256
-
257
- # Validate a channel
258
- config.registry.valid?(Channel.parse("public/standards")) # => true
259
- config.registry.valid?(Channel.parse("public/secret")) # => false
260
-
261
- # Locate config by walking up from a directory
262
- config = Metanorma::Release::ConfigLocator.find("/path/to/project")
263
- ----
264
-
265
- === Naming strategies
266
-
267
- Tag and file naming varies by document type. Strategies are resolved via a registry:
223
+ Tag and file naming varies by publisher (derived from the document identifier prefix). Strategies are resolved via a registry:
268
224
 
269
225
  [cols="1m,1,2",options="header"]
270
226
  |===
271
- |Document type |Strategy |Tag format
272
- |standard (default) |`EditionNaming` |`cc-18011/ed1`
273
- |IETF draft |`InternetDraftNaming` |`id-ietf-foo/1`
274
- |IETF RFC |`RfcNaming` |`rfc-1234/ed1`
275
- |IEEE |`DraftSuffixNaming` |`ieee-8021/d1`
276
- |IHO, OGC |`VersionNaming` |`iho-s44/v1`
227
+ |Publisher |Strategy |Tag format
228
+ |default (CalConnect, ISO) |`EditionSlug` |`cc-18011-2018/ed1`
229
+ |IETF draft |`InternetDraftSlug` |`id-ietf-foo/1`
230
+ |IETF RFC |`RfcSlug` |`rfc-1234/ed1`
231
+ |IEEE |`DraftSuffixSlug` |`ieee-8021/d1`
232
+ |IHO, OGC |`VersionSlug` |`iho-s44/v1`
277
233
  |===
278
234
 
279
- Register custom strategies:
280
-
281
- [source,ruby]
282
- ----
283
- registry = Metanorma::Release::NamingRegistry.default_registry
284
- registry.register("my-type", MyCustomNaming.new)
285
- ----
286
-
287
235
  === File routing
288
236
 
289
237
  The aggregation pipeline supports three file layout modes:
@@ -296,105 +244,46 @@ The aggregation pipeline supports three file layout modes:
296
244
  |`by-format` |`html/cc-18011.html`
297
245
  |===
298
246
 
299
- === Release manifest
300
-
301
- A `metanorma.release.yml` file controls which documents are published and to which channels:
302
-
303
- [source,yaml]
304
- ----
305
- config: myorg/.metanorma
306
- defaults:
307
- visibility: public
308
- channels:
309
- - public/standards
310
- documents:
311
- - source: sources/cc-18011.adoc
312
- channels:
313
- - public/standards
314
- - source: sources/cc-19060.adoc
315
- visibility: members
316
- channels:
317
- - members/early-access
318
- - pattern: "sources/draft-*.adoc"
319
- channels:
320
- - internal/working-drafts
321
- stages:
322
- - working-draft
323
- - committee-draft
324
- ----
325
-
326
- Documents not listed in the manifest use the `defaults` section. If no manifest exists, all documents are released as `public/standards`.
327
-
328
- Key fields:
329
-
330
- [cols="1m,3",options="header"]
331
- |===
332
- |Field |Description
333
- |`source` |Exact path match (highest priority)
334
- |`pattern` |Glob pattern match
335
- |`visibility` |`public`, `members`, or `private`
336
- |`channels` |List of target channels
337
- |`stages` |Allow-list of document stages
338
- |`config` |Channel config source (see <<channel-configuration>>)
339
- |===
340
-
341
- === Value objects
342
-
343
- All domain types are immutable, frozen, and use value-based equality:
344
-
345
- * `DocumentId` -- normalized document identifier (`CC 18011` -> `cc-18011`)
346
- * `DocumentVersion` -- edition + stage + pre-release flag
347
- * `DocumentStage` -- published, draft, working-draft, committee-draft, etc.
348
- * `Channel` -- audience/category pair
349
- * `ReleaseTag` -- tag string with pre-release flag
350
- * `ContentHash` -- SHA-256 content fingerprint
351
- * `RepoRef` -- owner/repo reference
352
-
353
- === Bibliography enrichment
247
+ == Architecture
354
248
 
355
- `RelatonEnricher` generates `index.json` and `index.yaml` from RXL (Relaton XML) files found in aggregated documents. It auto-detects the Relaton flavor from document metadata:
249
+ === Domain model
356
250
 
357
- [source,ruby]
358
- ----
359
- enricher = Metanorma::Release::RelatonEnricher.new(flavor: "calconnect")
360
- result = enricher.enrich(document_index, output_dir)
361
- # writes: output_dir/relaton/index.json
362
- # output_dir/relaton/index.yaml
363
- ----
251
+ All core types are immutable, frozen value objects:
364
252
 
365
- Flavor detection tries these gems in order: `relaton-calconnect`, `relaton-iso`, `relaton-iec`, `relaton-ogc`, `relaton-ietf`, and others. If a flavor gem is not installed, it falls back to `Relaton::Bib::Item` from the `relaton-bib` runtime dependency.
366
-
367
- == Architecture
253
+ * `Publication` -- metadata + files + channels + source
254
+ * `PublicationFile` -- format, name, path
255
+ * `PublicationSource` -- owner, repo, tag, url, date
256
+ * `Channel` -- string label wrapper
257
+ * `Index` -- collection of Publications with parameters
258
+ * `Site` -- aggregated output (index + file tree + Relaton enrichment)
368
259
 
369
260
  === Dependency flow
370
261
 
371
262
  Unidirectional, no cycles:
372
263
 
373
264
  ----
374
- domain/ -> release/ -> platform/
375
- -> aggregation/ -> platform/
376
- -> cli/
265
+ domain/ -> pipelines/ -> platform/
266
+ -> cli/commands/
377
267
  ----
378
268
 
379
- * `domain/` has zero knowledge of pipelines, platforms, or CLI
380
- * Pipelines depend on domain + interfaces, not platform implementations
381
- * Platform adapters depend on interfaces + domain, not pipelines
382
- * CLI delegates to command classes; commands depend on pipelines + platform factory
383
- * Commands use `ConfigResolver` mixin for channel config resolution
269
+ * Domain models have zero knowledge of pipelines, platforms, or CLI
270
+ * Pipelines receive all dependencies through constructors (dependency injection)
271
+ * Platform adapters implement interface modules (Publisher, RepoDiscoverer, ReleaseFetcher, etc.)
272
+ * CLI delegates to command classes; commands construct pipelines
384
273
 
385
274
  === Patterns
386
275
 
387
- Value Objects:: Immutable, frozen, value-based equality via `eql?`/`hash`. All fields included in equality comparison.
276
+ Value Objects:: Immutable, frozen, value-based equality via `eql?`/`hash`.
388
277
 
389
- Strategy Pattern:: Pluggable algorithms resolved via registry. Adding a new document type or platform requires zero changes to existing code.
278
+ Strategy Pattern:: Pluggable slug strategies resolved via registry. Adding a new publisher type requires zero changes to existing code.
390
279
 
391
280
  Pipeline with DI:: Pipelines receive all dependencies through constructors. No global state, no service locators.
392
281
 
393
- Null Object:: Disabled features inject null implementations (`NullDeltaState`, `NullPublisher`, `NullCacheStore`) instead of adding conditional checks.
282
+ Interface Modules:: Type contracts using `include Module` -- not duck typing. Dependencies are validated at construction time.
394
283
 
395
- Result Types:: Pipelines return frozen Structs. Errors are collected, not raised. The caller decides whether to abort.
284
+ Null Object:: Disabled features inject null implementations (`NullDeltaState`, `NullPublisher`, `NullCacheStore`).
396
285
 
397
- Command Pattern:: CLI delegates to `PackageCommand`, `PublishCommand`, and `AggregateCommand` classes. Each command encapsulates pipeline construction and configuration resolution via the `ConfigResolver` mixin.
286
+ Result Types:: Pipelines return frozen Structs. Errors are collected, not raised.
398
287
 
399
288
  === Extending
400
289
 
@@ -402,19 +291,16 @@ Command Pattern:: CLI delegates to `PackageCommand`, `PublishCommand`, and `Aggr
402
291
  |To add... |Do this
403
292
 
404
293
  |A new platform
405
- |Create a directory under `platform/` with `Publisher`, `Discoverer`, `Fetcher`, `ManifestReader` classes; register in `PlatformFactory`
294
+ |Create a directory under `platform/` with `Publisher`, `Discoverer`, `Fetcher`, `ManifestReader` classes that include the corresponding interface modules; register in `PlatformFactory`
406
295
 
407
- |A new naming strategy
408
- |Create a class that includes `NamingStrategy`; register via `NamingRegistry#register`
296
+ |A new slug strategy
297
+ |Create a class that includes `SlugStrategy`; register via `SlugRegistry#register`
409
298
 
410
299
  |A new file routing mode
411
- |Create a class with `#compute_path(file_name, metadata)`; register in `FileRoutingFactory`
300
+ |Create a class that includes `FileRouting` with a `#compute_path` method; register in `FileRoutingFactory`
412
301
 
413
302
  |A new filter
414
303
  |Create a class that includes `Filter`; pass to the pipeline's `filters` array
415
-
416
- |A new channel config source
417
- |Create a class that includes `ConfigFetcher` with a `#fetch(source)` method
418
304
  |===
419
305
 
420
306
  == Development
@@ -423,6 +309,7 @@ Command Pattern:: CLI delegates to `PackageCommand`, `PublishCommand`, and `Aggr
423
309
  ----
424
310
  bundle install
425
311
  bundle exec rspec
312
+ bundle exec rubocop
426
313
  ----
427
314
 
428
315
  == License
data/Rakefile CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'bundler/gem_tasks'
4
- require 'rspec/core/rake_task'
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
5
 
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'metanorma/release/cli'
4
+ require "metanorma/release/cli"
5
5
 
6
- Metanorma::Release::CLI.run(ARGV)
6
+ Metanorma::Release::CLI.start(ARGV)