scout-essentials 1.8.7 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.vimproject +26 -12
- data/README.md +83 -112
- data/VERSION +1 -1
- data/doc/Improvements.md +226 -0
- data/doc/StartHere.md +122 -0
- data/doc/developer/AnnotationSystem.md +184 -0
- data/doc/developer/Architecture.md +147 -0
- data/doc/developer/Configuration.md +238 -0
- data/doc/developer/CoreUtilities.md +265 -0
- data/doc/developer/DesignPrinciples.md +129 -0
- data/doc/developer/ErrorHandling.md +203 -0
- data/doc/developer/LockingAndConcurrency.md +157 -0
- data/doc/developer/PathResolution.md +200 -0
- data/doc/developer/PersistenceAndResources.md +119 -0
- data/doc/developer/StreamingModel.md +236 -0
- data/doc/user/AnnotatingData.md +202 -0
- data/doc/user/CachingResults.md +183 -0
- data/doc/user/CommandLineOptions.md +189 -0
- data/doc/user/Cookbook.md +211 -0
- data/doc/user/HandlingStreams.md +236 -0
- data/doc/user/LoggingAndProgress.md +158 -0
- data/doc/user/ProducingResources.md +177 -0
- data/doc/user/RemoteData.md +157 -0
- data/doc/user/RunningCommands.md +218 -0
- data/doc/user/WorkingWithFiles.md +217 -0
- data/lib/scout/cmd.rb +343 -40
- data/lib/scout/concurrent_stream.rb +14 -1
- data/lib/scout/indiferent_hash.rb +1 -1
- data/lib/scout/log/fingerprint.rb +13 -8
- data/lib/scout/log/progress/report.rb +1 -1
- data/lib/scout/log.rb +4 -1
- data/lib/scout/misc/digest.rb +6 -5
- data/lib/scout/misc/format.rb +24 -0
- data/lib/scout/named_array.rb +1 -1
- data/lib/scout/open/stream.rb +2 -2
- data/lib/scout/open/util.rb +8 -4
- data/lib/scout/open.rb +3 -3
- data/lib/scout/path/find.rb +3 -2
- data/lib/scout/persist.rb +14 -10
- data/lib/scout/resource/produce.rb +9 -1
- data/research/annotations-data-analysis.md +206 -0
- data/research/behavior-probes.md +1925 -0
- data/research/commands-streaming-analysis.md +272 -0
- data/research/design-philosophy-analysis.md +383 -0
- data/research/doc-audit-findings.md +294 -0
- data/research/ecosystem-attribution.md +118 -0
- data/research/implementation-inventory-core.md +1029 -0
- data/research/implementation-inventory-open.md +417 -0
- data/research/implementation-inventory-path-persist-resource.md +774 -0
- data/research/io-paths-analysis.md +228 -0
- data/research/persistence-resources-analysis.md +244 -0
- data/research/synthesis-report.md +80 -0
- data/scout-essentials.gemspec +37 -15
- data/test/scout/open/test_remote.rb +1 -2
- data/test/scout/test_cmd.rb +411 -0
- metadata +36 -14
- data/doc/Annotation.md +0 -352
- data/doc/CMD.md +0 -363
- data/doc/ConcurrentStream.md +0 -163
- data/doc/IndiferentHash.md +0 -240
- data/doc/Log.md +0 -235
- data/doc/NamedArray.md +0 -174
- data/doc/Open.md +0 -331
- data/doc/Path.md +0 -217
- data/doc/Persist.md +0 -214
- data/doc/Resource.md +0 -229
- data/doc/SimpleOPT.md +0 -236
- data/doc/TmpFile.md +0 -154
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# Annotation System
|
|
2
|
+
|
|
3
|
+
Annotations attach named metadata to **ordinary Ruby objects** without
|
|
4
|
+
wrapping them and without changing their class. The same mechanism is what
|
|
5
|
+
makes `Path`, `Resource` and `NamedArray` work, so understanding it is a
|
|
6
|
+
prerequisite for [Path Resolution](PathResolution.md) and
|
|
7
|
+
[Persistence and Resources](PersistenceAndResources.md).
|
|
8
|
+
|
|
9
|
+
Everything on this page is backed by `tmp/rewrite_C/probe_01..04.rb` and
|
|
10
|
+
probes P36–P42 in `research/behavior-probes.md`, run against the current
|
|
11
|
+
`lib/scout/annotation*.rb`.
|
|
12
|
+
|
|
13
|
+
## Load path
|
|
14
|
+
|
|
15
|
+
`lib/scout-essentials.rb` does **not** require `scout/annotation` directly.
|
|
16
|
+
`Annotation` becomes available because `scout/path` requires it
|
|
17
|
+
(`lib/scout/path.rb:1`), so `require 'scout-essentials'` gives you the
|
|
18
|
+
constant (`Object.const_defined?(:Annotation) => true`, probe_03) — but a
|
|
19
|
+
library that only wants annotations can `require 'scout/annotation'` on its
|
|
20
|
+
own.
|
|
21
|
+
|
|
22
|
+
## The DSL
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
module SampleInfo
|
|
26
|
+
extend Annotation
|
|
27
|
+
annotation :organism, :tissue
|
|
28
|
+
end
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`extend Annotation` turns the module into an **AnnotationModule**. The
|
|
32
|
+
`annotation` class-level call does three things (annotation_module.rb):
|
|
33
|
+
|
|
34
|
+
- declares the attribute list, kept in module state `@annotations` and read
|
|
35
|
+
with `SampleInfo.annotations` (there is **no `ANNOTATIONS` constant**);
|
|
36
|
+
- defines reader/writer methods (`#organism`, `#organism=`) for when the
|
|
37
|
+
module is mixed into an object;
|
|
38
|
+
- registers the module so `Annotation.setup` can resolve type names.
|
|
39
|
+
|
|
40
|
+
## `setup` and the binding rules
|
|
41
|
+
|
|
42
|
+
`AnnotationModule#setup(obj, values = nil, &block)` is the constructor. The
|
|
43
|
+
values may be given as a Hash or positionally, and a block is an alternative
|
|
44
|
+
source for the object itself:
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
SampleInfo.setup('S003', organism: 'Human', tissue: 'Liver')
|
|
48
|
+
SampleInfo.setup('S004', %w[Human Liver]) # positional, same order
|
|
49
|
+
SampleInfo.setup { 'S005' } # block-as-object
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Rules verified in probe_01/probe_02:
|
|
53
|
+
|
|
54
|
+
- **In-place extension.** `setup` calls `obj.extend SampleInfo` on the object
|
|
55
|
+
you pass and returns **that same object** (`setup(s).equal?(s) => true`) —
|
|
56
|
+
except when the object is **frozen**, in which case `obj.dup` is annotated
|
|
57
|
+
and returned instead; always use the return value of `setup`.
|
|
58
|
+
- **TypeError → un-annotated.** Objects that cannot hold singleton methods
|
|
59
|
+
(`Integer` literals, symbols) raise `TypeError: can't define singleton`;
|
|
60
|
+
`setup` rescues it and returns the *plain* object with no metadata.
|
|
61
|
+
- **`:annotation_types` is reserved.** Each annotated object gets an
|
|
62
|
+
`@annotation_types` array; declaring `annotation :annotation_types` collides
|
|
63
|
+
with it (probe_01: writing to it raised `NoMethodError` against `nil`).
|
|
64
|
+
|
|
65
|
+
## Introspection and serialisation
|
|
66
|
+
|
|
67
|
+
State lives on the object, the type list on the object too:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
s = SampleInfo.setup('S003', organism: 'Human', tissue: 'Liver')
|
|
71
|
+
|
|
72
|
+
SampleInfo.annotations # => [:organism, :tissue] (module state)
|
|
73
|
+
s.annotation_types # => [SampleInfo] (module objects!)
|
|
74
|
+
Annotation.is_annotated?(s) # => true
|
|
75
|
+
s.is_a?(SampleInfo) # => true (extend really happened)
|
|
76
|
+
s.is_a?(String) # => true (class unchanged)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`annotation_types` holds the **module objects**, never their names —
|
|
80
|
+
`s.annotation_types.include?(SampleInfo)` is the correct membership test.
|
|
81
|
+
|
|
82
|
+
`AnnotatedObject` (mixed into every annotated object) provides:
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
s.annotation_hash # => {:organism=>"Human", :tissue=>"Liver"}
|
|
86
|
+
s.annotation_info # => {:organism=>"Human", :tissue=>"Liver",
|
|
87
|
+
# :annotation_types=>[SampleInfo], :annotated_array=>false}
|
|
88
|
+
s.serialize # => {...same, :literal=>"S003"} (values purged recursively)
|
|
89
|
+
s.annotation_id # => "860c73f490edb8115064663b7f579d73"
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
- `annotation_hash` — the declared attributes only;
|
|
93
|
+
- `annotation_info` — plus `:annotation_types` and `:annotated_array`;
|
|
94
|
+
- `serialize` — `annotation_info` merged with `:literal` (the object itself),
|
|
95
|
+
with every value passed through `Annotation.purge` (annotated_object.rb:23);
|
|
96
|
+
this is what the `:annotation` serialisation driver consumes. It is **only a
|
|
97
|
+
Hash here**; the TSV form (`Annotation.tsv` / `Annotation.load_tsv`) lives in
|
|
98
|
+
scout-gear, not in this repo (attribution table in
|
|
99
|
+
[Architecture](Architecture.md));
|
|
100
|
+
- `annotation_id` (aliased `id`) — `Misc.digest([self, annotation_info])`.
|
|
101
|
+
|
|
102
|
+
### Module-level helpers
|
|
103
|
+
|
|
104
|
+
- `Annotation.is_annotated?(obj)` — true if any annotation module is mixed in.
|
|
105
|
+
- `Annotation.purge(obj)` — **recursive**: for an annotated object it calls the
|
|
106
|
+
instance `#purge`; for Arrays/Hashes it purges each element; otherwise it
|
|
107
|
+
returns the object unchanged.
|
|
108
|
+
- `Annotation.setup(obj, "A|B", hash)` — the generic deserialiser. The type
|
|
109
|
+
string is split on `|`, each name resolved with `Object.const_get`. **Unknown
|
|
110
|
+
names only warn and are skipped** (`Log.warn "Annotation NoSuchAnnotation not
|
|
111
|
+
defined"`, probe_01) — no exception.
|
|
112
|
+
|
|
113
|
+
### Instance-side helpers
|
|
114
|
+
|
|
115
|
+
- `#purge` — returns a **copy**: it removes `@annotation_types`, `@annotations`
|
|
116
|
+
and every attribute ivar from a `dup` (and purges nested values) and returns
|
|
117
|
+
it. The object's own class is kept (a purged `SampleInfo` String is still a
|
|
118
|
+
`String`, no longer `is_a?(SampleInfo)`); always use the return value.
|
|
119
|
+
- `#make_array` — wraps `self` in a **one-element Array** carrying the same
|
|
120
|
+
annotations and extends it with `AnnotatedArray` (annotated_object.rb:75-80);
|
|
121
|
+
it does not annotate the receiver's elements.
|
|
122
|
+
- `#annotate(other)` — copies the current annotations onto another object.
|
|
123
|
+
|
|
124
|
+
## Round-trips and copies
|
|
125
|
+
|
|
126
|
+
- **Marshal round-trips** annotations (P42, probe_01): the singleton modules
|
|
127
|
+
survive dump/load.
|
|
128
|
+
- **Only `dup` loses them; `clone` keeps them** (probe_02:
|
|
129
|
+
`dup is_annotated? => false`, `clone is_annotated? => true, .a => 1` —
|
|
130
|
+
`clone` copies the singleton class, `dup` does not): to re-annotate a `dup`
|
|
131
|
+
copy the metadata explicitly with `annotation_hash` →
|
|
132
|
+
`Annotation.setup` / `MyModule.setup`, or call `#annotate` on the copy.
|
|
133
|
+
|
|
134
|
+
## `AnnotatedArray`
|
|
135
|
+
|
|
136
|
+
`extend AnnotatedArray` on an annotated Array — the pattern used throughout
|
|
137
|
+
`test/scout/annotation/test_array.rb` — makes the *elements* carry the
|
|
138
|
+
container's annotations:
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
arr = SampleInfo.setup(%w[S001 S002], organism: 'Human')
|
|
142
|
+
arr.extend AnnotatedArray
|
|
143
|
+
arr[0].organism # => "Human"
|
|
144
|
+
arr.first.organism # => "Human"
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Overrides provided in `lib/scout/annotation/array.rb`:
|
|
148
|
+
|
|
149
|
+
- `[]` `(pos, clean = false)` — the element is re-annotated unless the second
|
|
150
|
+
argument is truthy, in which case it is returned clean (probe_09: a fresh
|
|
151
|
+
array's `fresh[0, true]` is an un-annotated `String` while `fresh[1]` is
|
|
152
|
+
annotated);
|
|
153
|
+
- `first`, `last`, `each_with_index`, `each`, `inject`, `collect`, `select` —
|
|
154
|
+
re-annotated;
|
|
155
|
+
- `compact`, `uniq`, `flatten`, `reverse`, `sort_by` — re-annotated;
|
|
156
|
+
- `subset(list)`, `remove(list)` — set operations (`&`, `-`) with
|
|
157
|
+
re-annotation.
|
|
158
|
+
|
|
159
|
+
**Limits** — live probe `tmp/rewrite_C/probe_09_annotated_array.rb` (method
|
|
160
|
+
owners plus actual results): `map`, `zip`, `filter_map`, `flat_map`,
|
|
161
|
+
`each_slice`, `values_at` and `count` are **not overridden** (their owner is
|
|
162
|
+
`Array`/`Enumerable`) and return plain results with no annotations — `ary.map
|
|
163
|
+
{ |x| x }` yields `[false, false, false]` under `Annotation.is_annotated?`,
|
|
164
|
+
while `ary.each` yields `[true, true, true]`, and `zip` keeps annotations only
|
|
165
|
+
on the container-side elements.
|
|
166
|
+
|
|
167
|
+
**Requirement:** elements must be extendable. `AnnotatedArray` over an Array
|
|
168
|
+
of `Integer`s raises `TypeError: can't define singleton` (probe_09) — use
|
|
169
|
+
Strings or other extendable objects.
|
|
170
|
+
|
|
171
|
+
## NamedArray is a separate thing
|
|
172
|
+
|
|
173
|
+
`NamedArray` (`lib/scout/named_array.rb`) is an Annotation module over Arrays
|
|
174
|
+
giving field-name access to positions. It does **not** extend
|
|
175
|
+
`AnnotatedArray` and is not a String type. It needs an explicit
|
|
176
|
+
`require 'scout/named_array'`. See [Annotating
|
|
177
|
+
Data](../user/AnnotatingData.md).
|
|
178
|
+
|
|
179
|
+
## Related
|
|
180
|
+
|
|
181
|
+
- [Path Resolution](PathResolution.md) — `Path` is an annotated module.
|
|
182
|
+
- [Persistence and Resources](PersistenceAndResources.md) — `Resource` is an
|
|
183
|
+
annotated module; `:annotation` serialisation uses `serialize`.
|
|
184
|
+
- [Annotating Data](../user/AnnotatingData.md) — the user-facing API.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
This page describes how the parts of scout-essentials fit together: the
|
|
4
|
+
module dependency graph, the load order, and where the boundaries to the
|
|
5
|
+
rest of the Scout ecosystem are. Everything here is derived from the source
|
|
6
|
+
under `lib/`.
|
|
7
|
+
|
|
8
|
+
## Modules and dependencies
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
IndiferentHash (no internal dependencies)
|
|
12
|
+
^ ^
|
|
13
|
+
| \-----------------\
|
|
14
|
+
Annotation (standalone) \
|
|
15
|
+
^ \
|
|
16
|
+
| \
|
|
17
|
+
Misc::Digest <- Path -----> Resource (annotation-based)
|
|
18
|
+
^ ^ ^ ^ ^
|
|
19
|
+
| | | | \
|
|
20
|
+
TmpFile --> Open --> CMD --> ConcurrentStream
|
|
21
|
+
^ ^
|
|
22
|
+
| |
|
|
23
|
+
Lockfile (required BY CMD, not the other way round)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Reading the edges as "requires":
|
|
27
|
+
|
|
28
|
+
- `IndiferentHash` requires nothing else internally.
|
|
29
|
+
- `Annotation` is a standalone mixin framework; `Path` and `Resource`
|
|
30
|
+
*extend* it, which is how `Path`/`Resource` metadata (pkgdir, libdir,
|
|
31
|
+
`@where`, `@original`, ...) attaches to plain Strings. This
|
|
32
|
+
**annotation-based unification** means a `Path` object is just a String
|
|
33
|
+
carrying annotations, and `Resource` is a module (not a class) whose
|
|
34
|
+
class-level state (`pkgdir`, `rake_dirs`, `path_maps`, `map_order`,
|
|
35
|
+
`lock_dir`) is read through those annotations.
|
|
36
|
+
- `Path` requires `Misc::Digest` (for `digest` naming) and the annotation
|
|
37
|
+
framework.
|
|
38
|
+
- `TmpFile` requires `Open`; the arrow therefore points **TmpFile → Open**
|
|
39
|
+
(not "Open depends on TmpFile").
|
|
40
|
+
- `Open` requires `Path` and `CMD`; `Open::Remote` shells out through
|
|
41
|
+
`CMD`. `Open` *uses* `TmpFile` helpers at runtime but does not require the
|
|
42
|
+
file.
|
|
43
|
+
- `CMD` requires `ConcurrentStream` (and `Open::Stream`); `ConcurrentStream`
|
|
44
|
+
does not require `CMD`.
|
|
45
|
+
- `Resource` requires `Log` and `Path` only — it does **not** depend on
|
|
46
|
+
`Persist`.
|
|
47
|
+
- `Persist` requires `Open` (and through it `CMD`); it never touches
|
|
48
|
+
`Resource`.
|
|
49
|
+
- `Log` colors come from `term-ansicolor`; there is **no `Log::Color`
|
|
50
|
+
constant**.
|
|
51
|
+
- The `Lockfile` implementation is **vendored** at
|
|
52
|
+
`lib/scout/open/lock/lockfile.rb` and used by `Open.lock`.
|
|
53
|
+
|
|
54
|
+
## Load order and entry points
|
|
55
|
+
|
|
56
|
+
`lib/scout-essentials.rb` is the **only** entry point; there is no
|
|
57
|
+
`lib/scout.rb`:
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
require_relative 'scout/exceptions'
|
|
61
|
+
require_relative 'scout/indiferent_hash'
|
|
62
|
+
require_relative 'scout/tmpfile'
|
|
63
|
+
require_relative 'scout/log'
|
|
64
|
+
require_relative 'scout/path'
|
|
65
|
+
require_relative 'scout/simple_opt'
|
|
66
|
+
require_relative 'scout/resource'
|
|
67
|
+
require_relative 'scout/resource/scout'
|
|
68
|
+
require_relative 'scout/persist'
|
|
69
|
+
require_relative 'scout/config'
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Notes on the cascade:
|
|
73
|
+
|
|
74
|
+
- `Annotation`, `Misc`, `CMD`, `ConcurrentStream`, `Open` and `Persist`'s
|
|
75
|
+
sub-files are pulled in indirectly (e.g. `scout/path` requires
|
|
76
|
+
`scout/annotation`; `scout/persist` requires `scout/persist/serialize`,
|
|
77
|
+
`scout/persist/open`, `scout/persist/path`).
|
|
78
|
+
- `scout/resource/scout` defines `Scout` (`extend Resource`, `pkgdir =
|
|
79
|
+
'scout'`, `Resource.default_resource = Scout`) and loads
|
|
80
|
+
`Path.load_path_maps(Scout.etc['path_maps'])`.
|
|
81
|
+
- `Misc::NamedArray` and `Misc::Hook` are **not** auto-loaded; they live in
|
|
82
|
+
`lib/scout/named_array.rb` and `lib/scout/misc/hook.rb` and are required
|
|
83
|
+
explicitly by the file that needs them.
|
|
84
|
+
- `scout/config` runs last and provides the `Scout::Config` module (not a top-level `Config` constant).
|
|
85
|
+
|
|
86
|
+
## Lock namespaces
|
|
87
|
+
|
|
88
|
+
There are three distinct lock directories, all under `$HOME/.scout/tmp`:
|
|
89
|
+
|
|
90
|
+
| Namespace | Constant/definition | Used by |
|
|
91
|
+
|-----------|---------------------|---------|
|
|
92
|
+
| `tmp/persist_locks` | `Persist.lock_dir` | `Persist.persist` while (re)computing a cache entry |
|
|
93
|
+
| `tmp/produce_locks` | `Resource.default_lock_dir` | `Resource#produce` while materializing a resource |
|
|
94
|
+
| `tmp/sensible_write_locks` | `Open.sensible_write_lock_dir` | `Open.sensible_write` atomic writes |
|
|
95
|
+
|
|
96
|
+
`Persist.lock` does **not exist** — the locking primitive is
|
|
97
|
+
`Open.lock(filename, &block)`, built on the vendored `Lockfile`.
|
|
98
|
+
|
|
99
|
+
## Fork model
|
|
100
|
+
|
|
101
|
+
`CMD` runs subprocesses through `Open3`, wiring stdout/stderr into
|
|
102
|
+
`ConcurrentStream`s so that streaming consumers can read while the process
|
|
103
|
+
is alive. Aborting a `CMD` kills the child process and aborts the attached
|
|
104
|
+
streams. `Resource#produce` for rake claims forks a `ScoutRake` invocation,
|
|
105
|
+
so a failed rake does not leave partial files behind
|
|
106
|
+
(`Open.sensible_write` removes them).
|
|
107
|
+
|
|
108
|
+
## Packaging
|
|
109
|
+
|
|
110
|
+
`scout-essentials.gemspec` is generated by juwelier from the `Juwelier::Tasks`
|
|
111
|
+
block in the `Rakefile` ("DO NOT EDIT THIS FILE DIRECTLY"). That block lists
|
|
112
|
+
four runtime dependencies — `term-ansicolor`, `yaml`, `rake`, `listen` — and
|
|
113
|
+
the checked-in gemspec mirrors them as `add_runtime_dependency` entries
|
|
114
|
+
(lines 172-175). Each is actually used from `lib/`: `term/ansicolor`
|
|
115
|
+
(`scout/log/color.rb`), `yaml` (`scout/persist/open.rb`,
|
|
116
|
+
`scout/log/progress/report.rb`), `rake` (`scout/resource/produce/rake.rb`),
|
|
117
|
+
`listen` (`scout/open/util.rb`, loaded lazily when a watcher is requested).
|
|
118
|
+
Nothing else is required at runtime; in particular scout-essentials does
|
|
119
|
+
not depend on scout-gear, rbbt-util, or any TSV/Workflow/Step
|
|
120
|
+
implementation.
|
|
121
|
+
|
|
122
|
+
## Ecosystem boundaries and attribution
|
|
123
|
+
|
|
124
|
+
scout-essentials is a **substrate library**. The following well-known Scout
|
|
125
|
+
concepts are **not** implemented here; each row says where they actually
|
|
126
|
+
live:
|
|
127
|
+
|
|
128
|
+
| Concept | Where it lives |
|
|
129
|
+
|---------|----------------|
|
|
130
|
+
| `TSV`, `TSV::Dumper`, `Annotation.tsv` | scout-gear (`scout/tsv`) |
|
|
131
|
+
| `Workflow`, `Task`, `Step`, `Step#info` / `.info` files | scout-gear (`scout/workflow`) |
|
|
132
|
+
| Job/step persistence beyond `Persist` (`.info` directory, `files/`) | scout-gear |
|
|
133
|
+
| HPC / scheduler (`slurm`, `lsf`, `pbs`, orchestrators) | scout-gear / scout-rig |
|
|
134
|
+
| `notify` / `send_email` helpers | scout-gear |
|
|
135
|
+
| `Bgzf` (blocked gzip) | scout-gear |
|
|
136
|
+
| `deep_indifferent` | **does not exist anywhere in the ecosystem** |
|
|
137
|
+
| Scout CLI (`scout` executable, `scout_commands/`) | scout-gear |
|
|
138
|
+
| rbbt-util shims (e.g. `rbbt-util` compatibility aliases) | UNVERIFIED — not present in this repo's `lib/` |
|
|
139
|
+
|
|
140
|
+
When writing documentation or examples for this repo, do not present those
|
|
141
|
+
as features of scout-essentials.
|
|
142
|
+
|
|
143
|
+
## Related
|
|
144
|
+
|
|
145
|
+
- [Path Resolution](PathResolution.md) for the `Path`/`Resource` machinery.
|
|
146
|
+
- [Persistence and Resources](PersistenceAndResources.md) for `Persist` and
|
|
147
|
+
`Resource#produce` contracts.
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
`Scout::Config` is the registry behind every option the Scout stack reads:
|
|
4
|
+
it is a flat `key -> [tokens, value]` store with one rule — **among the
|
|
5
|
+
entries whose token matches the caller's lookup context, the entry with the
|
|
6
|
+
LOWEST priority number wins**.
|
|
7
|
+
|
|
8
|
+
Source: `lib/scout/config.rb` (183 lines, whole file), plus
|
|
9
|
+
`lib/scout/resource/scout.rb` for `Scout.etc` (`Path.setup("etc")` — see
|
|
10
|
+
[PathResolution.md](PathResolution.md)).
|
|
11
|
+
|
|
12
|
+
## The token priority table
|
|
13
|
+
|
|
14
|
+
`Scout::Config.token_priority(token)` (`lib/scout/config.rb:38-67`) splits a
|
|
15
|
+
token on `::` and returns `[token, priority]`:
|
|
16
|
+
|
|
17
|
+
| token | priority | who sets it |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| `line:<file>:<line>` | 1 | derived from the caller of `Config.get` |
|
|
20
|
+
| `file:<file>` | 2 | derived from the caller of `Config.get` |
|
|
21
|
+
| `task:<name>` | 3 | passed by higher layers (e.g. a workflow task) |
|
|
22
|
+
| `workflow:<name>` | 4 | passed by higher layers |
|
|
23
|
+
| anything else | 10 | free-form custom tokens |
|
|
24
|
+
| `key:` / `key:<name>` | 20 | the key itself; LOWEST priority |
|
|
25
|
+
| `token::N` | N | explicit numeric suffix always wins over the defaults |
|
|
26
|
+
|
|
27
|
+
Verified by `tmp/rewrite_D/probe_11_token_probe.rb`:
|
|
28
|
+
|
|
29
|
+
```text
|
|
30
|
+
workflow:x => ["workflow:x", 4]
|
|
31
|
+
task:x => ["task:x", 3]
|
|
32
|
+
file:x => ["file:x", 2]
|
|
33
|
+
line:x => ["line:x", 1]
|
|
34
|
+
key:zz => ["key:zz", 20]
|
|
35
|
+
other:x => ["other:x", 10]
|
|
36
|
+
explicit ::5 => ["workflow:x", 5]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Lower number = higher precedence, because `Config.get` picks
|
|
40
|
+
`priorities.sort_by{|p,v| p}.first` (`lib/scout/config.rb:132`). The `key:`
|
|
41
|
+
token (20) is deliberately the weakest, so any context entry can override a
|
|
42
|
+
plain value.
|
|
43
|
+
|
|
44
|
+
## A worked example
|
|
45
|
+
|
|
46
|
+
A common misreading is "workflow overrides task". The numbers say the
|
|
47
|
+
opposite — `line` (1) outranks `workflow` (4). `tmp/rewrite_D/probe_13_get_internals.rb`
|
|
48
|
+
stacks all four tokens on one key and shows the chain end-to-end:
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
def fresh(k); Scout::Config::CACHE.delete(k); end
|
|
52
|
+
|
|
53
|
+
k='q1'; fresh k
|
|
54
|
+
Scout::Config.set({k=>'wfv'},'workflow:W')
|
|
55
|
+
Scout::Config.set({k=>'taskv'},'task:T')
|
|
56
|
+
Scout::Config.set({k=>'filev'},'file:F')
|
|
57
|
+
Scout::Config.set({k=>'linev'},'line:L')
|
|
58
|
+
Scout::Config.get(k,'workflow:W','task:T','file:F','line:L')
|
|
59
|
+
# => "linev" (line=1 wins)
|
|
60
|
+
|
|
61
|
+
k='q2'; fresh k
|
|
62
|
+
Scout::Config.set({k=>'wfv'},'workflow:W')
|
|
63
|
+
Scout::Config.set({k=>'taskv'},'task:T')
|
|
64
|
+
Scout::Config.set({k=>'filev'},'file:F')
|
|
65
|
+
Scout::Config.get(k,'workflow:W','task:T','file:F')
|
|
66
|
+
# => "filev" (file=2 wins)
|
|
67
|
+
|
|
68
|
+
k='q3'; fresh k
|
|
69
|
+
Scout::Config.set({k=>'wfv'},'workflow:W')
|
|
70
|
+
Scout::Config.set({k=>'taskv'},'task:T')
|
|
71
|
+
Scout::Config.get(k,'workflow:W','task:T')
|
|
72
|
+
# => "taskv" (task=3 wins)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Within one priority bucket the LAST `set` wins (`priorities[prio].unshift`,
|
|
76
|
+
`lib/scout/config.rb:83`; verified `probe_13` `two task entries => "second"`).
|
|
77
|
+
|
|
78
|
+
Explicit suffixes punch through the table, both ways — `::1` promotes an
|
|
79
|
+
entry above every default token, `::30` buries it below `key:` (probe_13,
|
|
80
|
+
cases `q6`/`q7`). Note the suffix is per-entry, not per-key: it re-prices one
|
|
81
|
+
`set` call, it does not change the token for later calls.
|
|
82
|
+
|
|
83
|
+
### Implicit caller tokens
|
|
84
|
+
|
|
85
|
+
Every `Config.get` appends `file:<caller>` and `line:<caller>:<n>` derived
|
|
86
|
+
from `caller` (`lib/scout/config.rb:119-127`), after filtering out frames
|
|
87
|
+
matching the legacy rbbt regexes (see *Legacy internals* below). Entries set
|
|
88
|
+
with those tokens match without being named in the call (verified
|
|
89
|
+
`probe_12`, `matched by implicit caller file token => "v"`).
|
|
90
|
+
|
|
91
|
+
## `etc/config` files
|
|
92
|
+
|
|
93
|
+
`Scout.etc.config` is `Path.setup("etc").config` — typically
|
|
94
|
+
`$HOME/.scout/etc/config`. Lines are `key value token...`, `#` comments,
|
|
95
|
+
whitespace-separated (`load_file`, `lib/scout/config.rb:15-25`):
|
|
96
|
+
|
|
97
|
+
```text
|
|
98
|
+
# comment
|
|
99
|
+
key value token
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`load_config` walks `Path.setup("etc").config.find_all.reverse`
|
|
103
|
+
(`lib/scout/config.rb:27-31`). `find_all` returns paths in `map_order`
|
|
104
|
+
(`current, user, home, local, global, usr, lib, fast, cache, bulk, ...`), so
|
|
105
|
+
the **reverse** order loads the *most specific* file (e.g. `$PWD/etc/config`)
|
|
106
|
+
**first** and `$HOME/.scout/etc/config` last. Two files setting the same key
|
|
107
|
+
with the same (implicit `key:`) token were tested by
|
|
108
|
+
`tmp/rewrite_D/probe_23_findall_order.rb`:
|
|
109
|
+
|
|
110
|
+
```text
|
|
111
|
+
map_order => [:current, :user, :home, ...]
|
|
112
|
+
find_all => [/tmp/.../etc/config, /home/.../.scout/etc/config]
|
|
113
|
+
loaded order => find_all.reverse (HOME first, CWD last)
|
|
114
|
+
CACHE['shared'] => [key-token fromHOME, key-token fromCWD]
|
|
115
|
+
get('shared') => "fromCWD-currentmap"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The `current` map wins even though it was loaded last, because `add_entry`
|
|
119
|
+
**appends** and `match` `unshift`s each matching value, so the last-loaded
|
|
120
|
+
entry lands at the *front* of the priority bucket and `get` takes
|
|
121
|
+
`priorities.sort_by.first`. In short: **later-loaded (more specific) files
|
|
122
|
+
win ties**.
|
|
123
|
+
|
|
124
|
+
`Config.set({key => value}, *tokens)` is the programmatic equivalent; with
|
|
125
|
+
one key it auto-adds the `key:<name>` token (`add_entry`,
|
|
126
|
+
`lib/scout/config.rb:11-19`).
|
|
127
|
+
|
|
128
|
+
## `Config.get` value post-processing
|
|
129
|
+
|
|
130
|
+
`Config.get(key, *tokens)` (`lib/scout/config.rb:94-155`) does four things
|
|
131
|
+
after resolving the winner:
|
|
132
|
+
|
|
133
|
+
1. **`'false'` becomes `false`** — the ONLY string-to-object coercion.
|
|
134
|
+
Verified in `tmp/rewrite_D/probe_10_env.rb`: `'false'` → `false`
|
|
135
|
+
(FalseClass), but `'TRUE'`, `'0'` and `'42'` all stay Strings.
|
|
136
|
+
2. **`'nil'` becomes `nil`** (probe_10).
|
|
137
|
+
3. **`env:VAR[,VAR2]`** in the resolved value is replaced by the first set
|
|
138
|
+
environment variable, and `get` returns `nil` if none is set (probe_10,
|
|
139
|
+
`'from-env'`).
|
|
140
|
+
4. Every `get` appends `[key, value, tokens]` to `GOT_KEYS` — an audit trail
|
|
141
|
+
of what was asked and answered (probe_10). `with_config` snapshots and
|
|
142
|
+
restores it.
|
|
143
|
+
|
|
144
|
+
Options hash as last argument:
|
|
145
|
+
|
|
146
|
+
- `:default => v` — used when no entry matches.
|
|
147
|
+
- `:env => 'VAR1,VAR2'` — first set env var becomes the default (probe_10).
|
|
148
|
+
|
|
149
|
+
A `Symbol` key raises `TypeError` — the cache is keyed by `key.to_s`, but
|
|
150
|
+
`CACHE[key.to_s]` is called after `match` has already used the raw key;
|
|
151
|
+
`Config.get(:symbol)` raises `TypeError: no implicit conversion of Symbol
|
|
152
|
+
into String` (probe_10). Use String keys.
|
|
153
|
+
|
|
154
|
+
## `with_config` and `process_config`
|
|
155
|
+
|
|
156
|
+
`with_config { ... }` (`lib/scout/config.rb:157-166`) dups every `CACHE`
|
|
157
|
+
bucket and `GOT_KEYS`, yields, and restores both in an `ensure`. Anything
|
|
158
|
+
`set` inside the block is rolled back — including sets made by code you
|
|
159
|
+
call (probe_10: `inside => "inside"`, `after => "outside"`). It is the
|
|
160
|
+
mechanism that keeps config changes from leaking out of a sub-operation.
|
|
161
|
+
|
|
162
|
+
`Config.process_config(config)` (`lib/scout/config.rb:168-180`) interprets
|
|
163
|
+
a CLI `--config` argument, in this order:
|
|
164
|
+
|
|
165
|
+
1. existing file path → `load_file`
|
|
166
|
+
2. name of an existing `Scout.etc.config_profile[config]` file → `load_file`
|
|
167
|
+
3. otherwise a `"key value token..."` string. The `::N` suffix is mandatory
|
|
168
|
+
in the stored token: any token without one gets `prio = "0"`, i.e. the
|
|
169
|
+
token is re-emitted as `token::0`.
|
|
170
|
+
|
|
171
|
+
Verified by `tmp/rewrite_D/probe_17b.rb` / `probe_17c.rb`. The subtle part
|
|
172
|
+
of case 3 is that the stored token keeps the suffix, so `process_config
|
|
173
|
+
"k3 cli workflow::0"` stores the token `workflow::0`, whose *name* is the
|
|
174
|
+
bare string `workflow` at priority 0 (stronger than `task`=3,
|
|
175
|
+
`file`=2, `line`=1, `key:`=20), and it does **not** match a call like
|
|
176
|
+
`get('k3', 'workflow:W')`, because the token *names* differ
|
|
177
|
+
(`workflow::0` vs `workflow:W`). Quoting a concrete outcome:
|
|
178
|
+
|
|
179
|
+
```text
|
|
180
|
+
CACHE[k3] => [[["workflow::0", "key:k3"], "cli"]]
|
|
181
|
+
token_priority('workflow::0') => ["workflow", 0]
|
|
182
|
+
get(k3) => "cli" (via the implicit key: token)
|
|
183
|
+
get(k3, 'workflow') => "cli" (prio 0 beats key:=20)
|
|
184
|
+
get(k3, 'workflow:W') => nil (token name mismatch)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
So a CLI `-config "key val workflow::0"` overrides anything except entries
|
|
188
|
+
carrying an explicit smaller `::N` — but only for lookups that pass the bare
|
|
189
|
+
`workflow` token.
|
|
190
|
+
|
|
191
|
+
## Environment variables read by the gem
|
|
192
|
+
|
|
193
|
+
Verified by grep over `lib/` and, where noted, executed probes
|
|
194
|
+
(`tmp/rewrite_D/probe_10_env.rb` covers the `env:`/`:env` machinery; the
|
|
195
|
+
rest are read directly from source):
|
|
196
|
+
|
|
197
|
+
| variable | read by | effect |
|
|
198
|
+
|---|---|---|
|
|
199
|
+
| `SCOUT_LOG` | `lib/scout/log.rb:35` | sets default log severity |
|
|
200
|
+
| `SCOUT_LOG_INSIST` | `lib/scout/misc/insist.rb:35` | logs each insist retry exception |
|
|
201
|
+
| `SCOUT_NOCOLOR` | `lib/scout/log/color.rb:137` (also `misc/format.rb:28`) | disables colors / unicode seconds glyph |
|
|
202
|
+
| `SCOUT_NO_PROGRESS` | `lib/scout/log/progress.rb:11` | disables progress bars |
|
|
203
|
+
| `SCOUT_ORIGINAL_STACK` | `lib/scout/log.rb:261,319` | keeps original exception backtrace |
|
|
204
|
+
| `SCOUT_DEBUG_PID` | `lib/scout/log.rb:178` | prefixes messages with the pid |
|
|
205
|
+
|
|
206
|
+
All are tested with `== 'true'` (except `SCOUT_LOG`, a severity name), so
|
|
207
|
+
any other value is false.
|
|
208
|
+
|
|
209
|
+
`Config.get` itself reads no env var unless `:env` is given or the value is
|
|
210
|
+
an `env:` string.
|
|
211
|
+
|
|
212
|
+
## Legacy internals
|
|
213
|
+
|
|
214
|
+
The caller-token filter in `Config.get` (`lib/scout/config.rb:119-127`)
|
|
215
|
+
still names `rbbt/...` files in its regexes. That is a leftover from the
|
|
216
|
+
upstream project this gem was extracted from; the effect here is only that
|
|
217
|
+
frames whose path matches those patterns are skipped when deriving
|
|
218
|
+
`file:`/`line:` tokens. Nothing else in `scout-essentials` depends on it
|
|
219
|
+
(marked INTERNAL in the audit matrix).
|
|
220
|
+
|
|
221
|
+
## Thread-safety
|
|
222
|
+
|
|
223
|
+
There is no mutex anywhere in `lib/scout/config.rb` (`Scout::Config` has no
|
|
224
|
+
`Mutex` ivar; verified `tmp/rewrite_D/probe_09_threads_fork.rb`). `CACHE`
|
|
225
|
+
and `GOT_KEYS` are plain shared structures and `add_entry` does
|
|
226
|
+
read-modify-write (`CACHE[key] ||= []; CACHE[key] << ...`). Concurrent
|
|
227
|
+
`set`/`get` from threads is therefore unprotected; the gem's own processes
|
|
228
|
+
are fork-based, not thread-based (see
|
|
229
|
+
[LockingAndConcurrency.md](LockingAndConcurrency.md)).
|
|
230
|
+
|
|
231
|
+
## Related pages
|
|
232
|
+
|
|
233
|
+
- [Architecture.md](Architecture.md) — where config sits in the layering.
|
|
234
|
+
- [CommandLineOptions.md](../user/CommandLineOptions.md) — the user-facing
|
|
235
|
+
side of `process_config`.
|
|
236
|
+
- [PathResolution.md](PathResolution.md) — `Scout.etc`, `find_all` order,
|
|
237
|
+
`etc/config` search path.
|
|
238
|
+
- [CachingResults.md](../user/CachingResults.md) — what reads `Config.get`.
|