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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/.vimproject +26 -12
  3. data/README.md +83 -112
  4. data/VERSION +1 -1
  5. data/doc/Improvements.md +226 -0
  6. data/doc/StartHere.md +122 -0
  7. data/doc/developer/AnnotationSystem.md +184 -0
  8. data/doc/developer/Architecture.md +147 -0
  9. data/doc/developer/Configuration.md +238 -0
  10. data/doc/developer/CoreUtilities.md +265 -0
  11. data/doc/developer/DesignPrinciples.md +129 -0
  12. data/doc/developer/ErrorHandling.md +203 -0
  13. data/doc/developer/LockingAndConcurrency.md +157 -0
  14. data/doc/developer/PathResolution.md +200 -0
  15. data/doc/developer/PersistenceAndResources.md +119 -0
  16. data/doc/developer/StreamingModel.md +236 -0
  17. data/doc/user/AnnotatingData.md +202 -0
  18. data/doc/user/CachingResults.md +183 -0
  19. data/doc/user/CommandLineOptions.md +189 -0
  20. data/doc/user/Cookbook.md +211 -0
  21. data/doc/user/HandlingStreams.md +236 -0
  22. data/doc/user/LoggingAndProgress.md +158 -0
  23. data/doc/user/ProducingResources.md +177 -0
  24. data/doc/user/RemoteData.md +157 -0
  25. data/doc/user/RunningCommands.md +218 -0
  26. data/doc/user/WorkingWithFiles.md +217 -0
  27. data/lib/scout/cmd.rb +343 -40
  28. data/lib/scout/concurrent_stream.rb +14 -1
  29. data/lib/scout/indiferent_hash.rb +1 -1
  30. data/lib/scout/log/fingerprint.rb +13 -8
  31. data/lib/scout/log/progress/report.rb +1 -1
  32. data/lib/scout/log.rb +4 -1
  33. data/lib/scout/misc/digest.rb +6 -5
  34. data/lib/scout/misc/format.rb +24 -0
  35. data/lib/scout/named_array.rb +1 -1
  36. data/lib/scout/open/stream.rb +2 -2
  37. data/lib/scout/open/util.rb +8 -4
  38. data/lib/scout/open.rb +3 -3
  39. data/lib/scout/path/find.rb +3 -2
  40. data/lib/scout/persist.rb +14 -10
  41. data/lib/scout/resource/produce.rb +9 -1
  42. data/research/annotations-data-analysis.md +206 -0
  43. data/research/behavior-probes.md +1925 -0
  44. data/research/commands-streaming-analysis.md +272 -0
  45. data/research/design-philosophy-analysis.md +383 -0
  46. data/research/doc-audit-findings.md +294 -0
  47. data/research/ecosystem-attribution.md +118 -0
  48. data/research/implementation-inventory-core.md +1029 -0
  49. data/research/implementation-inventory-open.md +417 -0
  50. data/research/implementation-inventory-path-persist-resource.md +774 -0
  51. data/research/io-paths-analysis.md +228 -0
  52. data/research/persistence-resources-analysis.md +244 -0
  53. data/research/synthesis-report.md +80 -0
  54. data/scout-essentials.gemspec +37 -15
  55. data/test/scout/open/test_remote.rb +1 -2
  56. data/test/scout/test_cmd.rb +411 -0
  57. metadata +36 -14
  58. data/doc/Annotation.md +0 -352
  59. data/doc/CMD.md +0 -363
  60. data/doc/ConcurrentStream.md +0 -163
  61. data/doc/IndiferentHash.md +0 -240
  62. data/doc/Log.md +0 -235
  63. data/doc/NamedArray.md +0 -174
  64. data/doc/Open.md +0 -331
  65. data/doc/Path.md +0 -217
  66. data/doc/Persist.md +0 -214
  67. data/doc/Resource.md +0 -229
  68. data/doc/SimpleOPT.md +0 -236
  69. data/doc/TmpFile.md +0 -154
@@ -0,0 +1,1029 @@
1
+ # Implementation inventory — CORE chunk (audit chunk 1)
2
+
3
+ Evidence-backed inventory of the core utility subsystems of `scout-essentials`
4
+ (v1.8.8). Every claim carries a `file:line` anchor. Disputed semantics were
5
+ settled with `ruby -Ilib` probes (see `research/behavior-probes.md`).
6
+
7
+ Entry point facts that frame everything else:
8
+
9
+ - `lib/scout-essentials.rb:1-10` — the only load entry. Requires in order:
10
+ `scout/exceptions`, `scout/indiferent_hash`, `scout/tmpfile`, `scout/log`,
11
+ `scout/path`, `scout/simple_opt`, `scout/resource`, `scout/resource/scout`,
12
+ `scout/persist`, `scout/config`.
13
+ - `lib/scout.rb` **does not exist** (verified: `ls lib/scout.rb` → No such file).
14
+ There is no `Scout` module file; `module Scout` appears only inside
15
+ `lib/scout/config.rb:5` (as `module Scout::Config`, re-opening `Scout` created
16
+ by `resource/scout.rb`) and `lib/scout/resource/scout.rb:1` (`module Scout;
17
+ extend Resource; self.pkgdir = 'scout'`).
18
+ - Consequently: `NamedArray` and `Hook` are **not** loaded by
19
+ `require 'scout-essentials'` (they need explicit
20
+ `require 'scout/named_array'` / `'scout/misc/hook'`; probe P5).
21
+ `Misc`, `TmpFile`, `Log`, `SOPT`, `IndiferentHash` **are** loaded — and so
22
+ is `CMD` (transitively, via the `open`/`tmpfile` require chain; probe P5
23
+ and follow-up `Object.const_defined?(:CMD) => true`).
24
+ - `lib/scout/tmpfile.rb:1` requires `open` (pulling `ConcurrentStream`, `Open`),
25
+ `misc`, `log` — so `misc.rb` and `open.rb` are load-order ancestors of
26
+ `scout-essentials`.
27
+
28
+ ---
29
+
30
+ ## 1. `lib/scout-essentials.rb` (10 lines)
31
+
32
+ Pure require cascade; no code. Ordering above.
33
+
34
+ What downstream consumes: the `require 'scout-essentials'` idiom itself, plus
35
+ the implicit guarantee that after it `Log`, `Misc`, `TmpFile`, `IndiferentHash`,
36
+ `SOPT`, `Path`, `Scout::Config`, `Persist`, `Scout` are defined.
37
+
38
+ Subtlety worth documenting: after a bare `require 'scout-essentials'` the
39
+ defined constants include `Misc`, `TmpFile`, `Log`, `SOPT`,
40
+ `IndiferentHash`, `Scout::Config` **and `CMD`** (CMD is pulled in
41
+ transitively, see probe P5), while **`NamedArray` and `Hook` are NOT defined**
42
+ (`tmpfile` does not require `named_array`; `misc.rb` does not require
43
+ `misc/hook`). Code assuming either is present must add
44
+ `require 'scout/named_array'` / `require 'scout/misc/hook'`.
45
+
46
+ ---
47
+
48
+ ## 2. `Scout::Config` — `lib/scout/config.rb` (184 lines)
49
+
50
+ ### Structure
51
+ - `module Scout::Config` (`config.rb:5`) — class methods only (module with
52
+ `self.` defs, no instances).
53
+ - State: `CACHE ||= IndiferentHash.setup({})` (`:7`) mapping
54
+ `key_string => [[tokens_array, value], ...]`; `GOT_KEYS = []` (`:9`) audit
55
+ trail of `[key, value, tokens]` tuples appended on every `get` (`:136`).
56
+ - Loads at require time: `self.load_config` (`:183`).
57
+
58
+ ### Key public methods
59
+ - `add_entry(key, value, tokens)` (`:11-16`) — appends `[tokens, value]` under
60
+ `key.to_s`; always ensures `key:<key>` is among tokens (`:13`).
61
+ - `load_file(file)` (`:18-26`) — line format `key value token token ...` split
62
+ on whitespace; `#`-prefixed lines skipped (`:21`); **empty `key` lines are
63
+ skipped** (`:24` — `if key`). Each line becomes one entry.
64
+ - `load_config` (`:28-32`) — `Path.setup("etc").config.find_all.reverse.each`,
65
+ i.e. all `config` files in the `etc` search path, later files override by
66
+ order of insertion into CACHE (not by load order semantics — ordering matters
67
+ only via `unshift`/`concat` in `match`, see below).
68
+ - `set(values, *tokens)` (`:34-42`) — either a Hash of pairs, or
69
+ `set(key, value, *tokens)` (non-Hash first arg → `{values => tokens.shift}`).
70
+ - `token_priority(token)` (`:44-68`) — token format `name[::N]`:
71
+ explicit `::N` numeric priority wins (`:63-65`); else inferred by prefix:
72
+ `workflow`→4, `task`→3, `file`→2, `line`→1, `key`→20, else 10 (`:49-62`).
73
+ Returns `[token_name_without_priority, priority]`.
74
+ - `match(entries, give_token)` (`:70-87`) — builds `priorities[prio] => [values]`
75
+ for entries whose tokens include `give_token`; **`unshift`s values**
76
+ (`:83`), so later-loaded entries surface first at equal priority.
77
+ - `get(key, *tokens)` (`:90-150`) — resolution algorithm:
78
+ 1. pop trailing Hash options; `:default` and `:env` (`:91-95`);
79
+ 2. `:env` — first set var among comma-split names becomes `default`
80
+ (`:97-104`);
81
+ 3. `tokens = ["key:" + key] if tokens.empty?` (`:106`) — **requires String
82
+ key**; a Symbol key here raises `TypeError` (probe P3a: `no implicit
83
+ conversion of Symbol into String`);
84
+ 4. caller inspection adds `file:<path>` and `line:<path>:<lineno>` tokens
85
+ (`:109-120`), filtering frames matching rbbt-era paths
86
+ (`rbbt/(resource.rb|workflow.rb)`, `rbbt/resource/path.rb`,
87
+ `rbbt/util/misc.rb`, `accessor.rb`, `progress-monitor.rb`, `:110-115`) —
88
+ **legacy rbbt regexes, not scout paths**; the caller token logic therefore
89
+ rarely filters anything in scout code today;
90
+ 5. all matching tokens collapse into `priorities` (`:122-130`);
91
+ 6. `value = priorities.collect{|p| p }.sort_by{|p,v| p}.first.last.first`
92
+ (`:132`) — **lowest priority number wins**, and among a priority bucket
93
+ the **first** value wins (i.e. the last-loaded one, due to unshift);
94
+ 7. `'false'` → `false` (`:133`);
95
+ 8. `GOT_KEYS << [key, value, tokens]` (`:136`);
96
+ 9. value `env:VAR1,VAR2` → resolves to first set ENV var, else `nil`
97
+ (`:138-144`); literal `'nil'` → `nil` (`:145`).
98
+ - `with_config` (`:152-164`) — snapshot `CACHE` (deep-dup of value arrays) and
99
+ `GOT_KEYS`, restore in `ensure`.
100
+ - `process_config(config)` (`:166-180`) — CLI-oriented: if `config` is a
101
+ filename that exists → `load_file`; elsif `Scout.etc.config_profile[config]`
102
+ exists → load that; else parse `key value tok::prio ...` and `set`.
103
+
104
+ ### Verified semantics (probes)
105
+ - P3: with entries `key:tk` ("plain"), `file:/some/file.rb` ("filetok"),
106
+ `line:/some/file.rb:1` ("linetok"), `workflow::0` ("wftok") all present,
107
+ `get("tk")` → `"wftok"` (probe P16 adds bare-vs-file). Note `workflow::0` (priority 0) beats
108
+ `line:` (1) and `file:` (2); but the generic `key:` token has priority 20,
109
+ i.e. the **lowest precedence** of all — the implicit key token only ever wins
110
+ when nothing else matches.
111
+ - P3b: `get("k7", "key:k7")` with a `key:k7` entry → `"high"`; explicit tokens
112
+ work. `get("k4", "key:k4")` on missing key → `nil` (no default). `get("k5")
113
+ ` with `'false'` value → `false`. `get("k6")` with value `env:NOPE1,NOPE2`
114
+ → `nil` (env fallback unset).
115
+ - P3b: `Path.setup("etc").config.find_all` → `[]` in a clean checkout (no
116
+ `etc/config` in the search path), so `load_config` is a no-op here — all
117
+ runtime config comes from `set`/`process_config`/ENV.
118
+
119
+ ### Error handling / guarantees
120
+ - No exceptions raised on unknown keys (`get` returns default/nil).
121
+ - `get` with Symbol key and no tokens: TypeError from `:106` (documented
122
+ footgun).
123
+ - `with_config` restores both caches even on raise.
124
+
125
+ ### Concurrency
126
+ - No mutex anywhere in config.rb; CACHE is shared mutable state.
127
+
128
+ ### External integrations / env vars
129
+ - Reads arbitrary ENV vars via `:env` option (`:98-103`) and `env:` value
130
+ prefix (`:138-144`).
131
+
132
+ ### Subtle points (docs plausibly wrong)
133
+ - The **priority scheme is inverted from intuition**: lower number = higher
134
+ precedence, and the implicit `key:` token has the **worst** priority (20),
135
+ so a config line with *any* explicit token overrides a bare `key value`
136
+ line. `workflow` (4) beats `task` (3)? No — `task` (3) beats `workflow` (4);
137
+ but explicit `::0` beats everything.
138
+ - Doc claims about "later files override earlier" are only half-true: within a
139
+ priority bucket the *later inserted* (unshifted) value wins
140
+ (`config.rb:83`), and `load_config` reads files in **reverse** find order
141
+ (`:29`) so the last file in the path is loaded first — combined effect is
142
+ that the *first* file in the path search order wins at equal priority.
143
+ - caller-based `file:`/`line:` tokens are derived from the call site of `get`,
144
+ filtered by rbbt-legacy regexes (`:110-115`), so in scout code they point at
145
+ the scout caller line — any doc describing per-workflow config file
146
+ precedence needs this exact rule.
147
+
148
+ ---
149
+
150
+ ## 3. `IndiferentHash` — `lib/scout/indiferent_hash.rb` (177) + submodules
151
+
152
+ ### Structure
153
+ - `module IndiferentHash` used as an **extension** (`self.setup(hash)` does
154
+ `hash.extend IndiferentHash`, `indiferent_hash.rb:7-10`). Not a subclass of
155
+ Hash — an extended plain Hash.
156
+ - Submodules: `indiferent_hash/options.rb` (module-level helpers),
157
+ `case_insensitive.rb` (separate `CaseInsensitiveHash` extension),
158
+ `serialize.rb` (`IndiferentHash.serializable`).
159
+
160
+ ### Instance protocol (indiferent_hash.rb)
161
+ - `merge(other)` (`:12-19`) — returns new IndiferentHash, other's keys win.
162
+ - `deep_merge(other)` (`:21-32`) — recursive only when both sides are Hash and
163
+ the existing value is already `IndiferentHash` (`:25`).
164
+ - `[]=(key,value)` (`:34-37`) — **deletes any dual-form key first** (`:35`), so
165
+ writing `h[:a]=1` after `h["a"]=2` leaves a single entry.
166
+ - `[](key)` (`:43-61`) — tries the literal key, then the Symbol↔String swap
167
+ (`:51-58`); nested Hash results get `IndiferentHash.setup` (`:47,59`). Guard
168
+ `_default?` (`:39-41`) avoids returning a default-proc value that isn't a
169
+ real key.
170
+ - `values_at(*keys)` (`:63-65`), `include?` (`:67-76`), `delete` (`:78-89`) —
171
+ all dual-form aware.
172
+ - `clean_version` (`:91-97`) — plain Hash with String keys; **first** key form
173
+ wins on collision (`:94`), so `{"a"=>1, :a=>2}.clean_version` keeps 1 (P1).
174
+ - `slice(*list)` (`:99-114`) — expands each Symbol/String into both forms,
175
+ returns IndiferentHash.
176
+ - `keys_to_sym!` (`:116-125`) / `keys_to_sym` (`:127-138`).
177
+ - `pretty_print` (`:140-142`) → `Misc.format_definition_list(self, sep: "\n")`.
178
+ - `except(*list)` (`:144-160`) — dual-form removal.
179
+ - `dig(*keys)` (`:162-171`) — iteratively setups nested hashes.
180
+ - `self.dig(obj, *keys)` (`:173-176`).
181
+
182
+ ### Module helpers (options.rb)
183
+ - `add_defaults(options, defaults = {})` (`:2-15`) — String inputs parsed via
184
+ `string2hash`; does **not** overwrite existing keys (`:9`).
185
+ - `process_options(hash, *keys)` (`:17-28`) — **destructive**: pops the
186
+ requested keys out of `hash` (P2/P6: after processing, `h` retains only the
187
+ un-processed keys) and returns one value or an array. Trailing Hash = defaults.
188
+ - `pull_keys(hash, prefix)` (`:30-51`) — extracts `prefix_options` plus every
189
+ `<prefix>_<rest>` key into a new IndiferentHash, deleting from source.
190
+ - `zip2hash(list1, list2)` (`:53-59`), `positional2hash(keys, *values)`
191
+ (`:61-72`) — merges a trailing Hash of extras (defaults), drops nil/"" values
192
+ and unknown keys (`:65-67`).
193
+ - `array2hash(array, default = nil)` (`:74-81`) — `[k,v]` pairs; `default.dup`
194
+ fills nil values (raises if default lacks `dup`).
195
+ - `process_to_hash(list)` (`:83-86`) — `zip2hash(list, yield(list))`.
196
+ - `hash2string(hash)` (`:88-94`) — `k=v` joined with `#`; only
197
+ Symbol/String/Float/Integer/Numeric/True/False/Module/Class/Object values kept
198
+ (`:90`); Symbol keys get `:` prefix (`:91`). Note `Fixnum` in the whitelist
199
+ is a Ruby<2.4 relic.
200
+ - `string2hash(string, sep="#")` (`:96-116`) — `#`-separated `k=v` pairs.
201
+ Value coercion order (`:104-112`): empty→`true`; `:x`→Symbol; `/re/`→Regexp;
202
+ `'x'`/`"x"`→unquoted String; `\d+`→Integer; `\d*\.\d+`→Float; `"true"`→`true`;
203
+ `"false"`→**stays the String `"false"` (false-bug, see below)**; else String.
204
+ - `parse_options(str)` (`:118-154`) — whitespace-separated `k=v`, quoted values
205
+ keep spaces, comma values split into Arrays preserving quotes (`:130-140`).
206
+ Scalar coercions identical to `string2hash`, including the same false-bug.
207
+ List-valued options (`a=1,2`) become Arrays of Strings and skip the scalar
208
+ coercions entirely.
209
+ - `print_options(options)` (`:156-173`) — inverse-ish of parse_options:
210
+ arrays become `k=a,b` with quoted elements containing spaces; scalar values
211
+ quoted when empty or containing spaces.
212
+
213
+ ### CaseInsensitiveHash (case_insensitive.rb)
214
+ - `self.setup(hash)` extend-based (`:3-5`); `downcase_keys` memoized map
215
+ downcased→original (`:7-15`); `[](key, *rest)` falls back to the original
216
+ key on miss (`:17-22`); `values_at` (`:24-28`). Read-only convenience — no
217
+ `[]=`/`delete`/`include?` overrides.
218
+
219
+ ### serialize.rb
220
+ - `self.serializable(obj)` (`:2-23`) — deep copy of Hashes, Arrays >100
221
+ truncated to first 70 + last 30 + `['...', 'TRUNCATED only 100 out of N
222
+ shown']` (`:13-18`).
223
+
224
+ ### Probes
225
+ - P1: symbol/string interchange, slice, merge, clean_version first-wins, dig.
226
+ - P2: `string2hash` vs `parse_options` coercion table (P2's `g=false` line
227
+ predates the false-bug investigation; P20 supersedes it).
228
+ - P2: `process_options` destructiveness (`h.keys` shrinks).
229
+
230
+ ### Subtle points
231
+ - **`"false"` never coerces to `false`** in either `string2hash` or
232
+ `parse_options` (`options.rb:110-111` and `:148-149`): the idiom
233
+ `options[key] = false and next if value == "false"` assigns `false`, but the
234
+ assignment itself evaluates to falsy `false`, so `and next` does not fire and
235
+ the trailing `options[key] = value` overwrites it with the String `"false"`
236
+ (probe P20; verified against the installed gem copy too). `"true"` coerces
237
+ correctly only by luck. `Scout::Config.get` *does* coerce the String
238
+ `'false'` (config.rb:133), so the gem's two boolean-from-string mechanisms
239
+ disagree.
240
+ - `clean_version` keeps the **first** key encountered, not "string wins" —
241
+ with `{:a=>1,"a"=>2}` the Symbol survives (P1: `["a","b"]` after clean on
242
+ `{"a"=>1,:b=>2}` was a different case; the general rule is `each` order,
243
+ `:91-96`).
244
+ - `process_options` **mutates** its argument; docs claiming a pure accessor
245
+ would be wrong.
246
+ - (superseded by the bullet above — the two parsers actually agree, both fail
247
+ to produce `false`; the real asymmetry is with `Scout::Config.get`.)
248
+ - `IndiferentHash` is an extension, so `IndiferentHash === h` is false for a
249
+ plain Hash and `h.class` stays `Hash`.
250
+
251
+ ---
252
+
253
+ ## 4. `NamedArray` — `lib/scout/named_array.rb` (165 lines)
254
+
255
+ ### Structure
256
+ - `module NamedArray; extend Annotation; annotation :fields, :key` (`:2-4`) —
257
+ an Annotation module applied to Arrays. Requires `annotation` (`:1`).
258
+ - Loaded **only** via explicit `require 'scout/named_array'` (P5: NameError
259
+ under bare `scout-essentials`).
260
+
261
+ ### Class methods
262
+ - `field_match(field, name)` (`:10-20`) — tolerant equality: exact; `"(name)"`
263
+ inside field or vice-versa; prefix followed by space. Non-Strings compared
264
+ with `==`.
265
+ - `identify_name(names, selected, strict: false)` (`:22-57`) — maps a field
266
+ selector to an index (or Range/Integer passthrough, `:27-30`); `Symbol :key`
267
+ → `:key` (`:32`); `Symbol` otherwise recurses via `to_s` (`:32`);
268
+ Strings: exact index (`:42-43`), numeric-string → Integer (`:44-46`),
269
+ `strict` stops (`:47`), else `field_match` scan (`:48-49`), else nil. Note
270
+ dead branch `when (names.nil? and String)` (`:33-38`): if `names` is nil
271
+ and the selector is a String, the code calls
272
+ `identify_field(key_field, fields, ...)` on **undefined locals**, so the
273
+ branch would raise NameError if ever reached. Latent bug, noted for docs.
274
+ - `_zip_fields(array, max = nil)` (`:113-125`) / `zip_fields(array)`
275
+ (`:127-144`, slices at 10000) / `add_zipped(source, new)` (`:146-152`).
276
+ **Line `:119` is dead code** (P15/P15b): `v.length == 1 & max > 1` parses
277
+ as `v.length == ((1 & max) > 1)` — `1 & max` is always 0/1 so
278
+ `(1 & max) > 1` is always false, and singleton columns other than the
279
+ first are NEVER repeated (observed: nil padding). Only the first column
280
+ (`:126`, `first.length == 1 and max > 1`) can be broadcast — asymmetric
281
+ behavior docs must not "fix" into symmetry.
282
+
283
+ ### Instance protocol
284
+ - `all_fields` (`:6-8`), `identify_name(selected)` (`:59-61`),
285
+ `positions(fields)` (`:63-71`), `[](key)`/`[]=(key,value)` by field name
286
+ (`:73-83`, returning nil silently when the field is unknown),
287
+ `concat(other)` handling Hash or NamedArray (`:86-99`), `to_hash`
288
+ (`:101-107`, IndiferentHash), `values_at(*positions)` (`:109-111`),
289
+ `method_missing` for field-name accessors (`:154-160`), `pretty_print`
290
+ (`:162-164`).
291
+
292
+ ### Probes (P15)
293
+ - `NamedArray.setup([1,2], [:a,:b])` → `.a`→1, `.b`→2, `.to_hash`→
294
+ `{:a=>1,:b=>2}`; `identify_name([:a,:b], "b")`→1, `"zzz"`→nil;
295
+ `positions("b")`→1, `[:a,:b]`→[0,1]; `field_match` tolerance confirmed
296
+ (0 for exact, nil for unrelated).
297
+
298
+ ### Subtle points
299
+ - `[]=` silently no-ops on unknown field (`:81-82`).
300
+ - Not auto-loaded (footgun for docs).
301
+ - `_zip_fields` boolean/bitwise expression at `:119`.
302
+
303
+ ---
304
+
305
+ ## 5. `Misc` — `lib/scout/misc.rb` (11 lines) + 10 submodules
306
+
307
+ `misc.rb:1-9` requires format, insist, digest, filesystem, monitor, system,
308
+ helper, matching, math — **note `hook.rb` is NOT required** by `misc.rb`
309
+ (verified: `misc.rb:1-9` list). `Hook` is a separate top-level module.
310
+
311
+ ### 5.1 misc/format.rb (310 lines) — string/number formatting
312
+ - `COLOR_LIST` (`:2`), `colors_for(list)` (`:4-19`) assigns colors in order,
313
+ memoizing per distinct element, returns `[colors, used]`.
314
+ - `format_seconds(time, extended = false)` (`:21-26`) → `HH:MM:SS` (+`.cs`).
315
+ - `CHAR_SENCONDS` (`:28`, typo for SECONDS; `"″"` unless `SCOUT_NOCOLOR`) and
316
+ `format_seconds_short` (`:29-37`).
317
+ - `MAX_TTY_LINE_WIDTH = 120` (`:39`).
318
+ - `format_paragraph(text, size, indent, offset)` (`:40-73`) — wraps text at
319
+ `size` (default `Log.tty_size || 120`, capped at 120), preserving
320
+ paragraph-like separators (blank lines, markdown `*`/`-`/```/leading spaces)
321
+ via the regex at `:48`; long words truncated with `...` (`:57`).
322
+ - `format_definition_list_item(dt, dd, indent, size, color: :yellow)`
323
+ (`:75-100`) and `format_definition_list(defs, ...)` (`:102-111`) — used by
324
+ `IndiferentHash#pretty_print`, `NamedArray#pretty_print`, SOPT docs.
325
+ - `camel_case` (`:113-118`, note the misleading `return` inside a guard:
326
+ `return string if string !~ /_/ && string =~ /[A-Z]+.*/`),
327
+ `camel_case_lower` (`:120-124`), `snake_case` (`:126-134`).
328
+ Probes: `snake_case("SomeCamelCase")`→"some_camel_case";
329
+ `camel_case("some_snake_case")`→"SomeSnakeCase";
330
+ `camel_case("ABCdef")`→"ABCdef" (acronym preserved);
331
+ `camel_case_lower`→"someSnakeCase".
332
+ - `humanize(value, options = {})` (`:138-178`) — formats `:sentence` (default),
333
+ `:allcaps`, `:class`, `:nocaps`; acronym-aware (Miguel Vazquez edit note at
334
+ `:146`). Probes: "some_field_name"→"Some field name";
335
+ `format: :class`→"SomeFieldName"; "ABC_acronym_x"→"ABC acronym x".
336
+ - `fixascii`/`to_utf8`/`fixutf8` (`:180-204`) — encoding repair;
337
+ `fixutf8` has an operator-precedence-laden guard at `:194-195`.
338
+ - `humanize_list(list)` (`:206-213`).
339
+ - `human_number(n)` (`:215-237`) — K/M/B/T units. Probes: 0→"0", 950→"950",
340
+ 1234→"1.2K", -2500000→"-2.5M".
341
+ - `parse_sql_values(txt)` (`:239-277`) — minimal INSERT-values parser.
342
+ Probe: `('a','b,c'),(1,2)` → `[["a","b,c"],["1","2"]]` (all values Strings).
343
+ - `timespan(str, default = "s")` (`:279-309`) — leading `-` negates
344
+ (`:281`); `HH:MM:SS`/`MM:SS` handled (`:283-286`, note the
345
+ `seconds, minutes, hours` naming of the reversed split — a 2-part
346
+ `"01:02"` string is minutes:seconds); token table `:288-300` (`s,sec,m,min,
347
+ ','',h,d,w,mo,y`); `tokens[nil]`/`tokens[""]` default (`:302-303`);
348
+ aggregates `(\d+)(\w*)` pairs (`:305-307`).
349
+ - **Compound timespans and unknown units raise `TypeError`** (P7/P15,
350
+ confirmed against the library itself):
351
+ `Misc.timespan("1h30m")` and `Misc.timespan("1x")` both raise
352
+ `TypeError: nil can't be coerced into Integer`, because the scan yields the
353
+ unit string `"h30m"` (or `"x"`) which is absent from the token table, and
354
+ `amount.to_i * nil` blows up. Working forms: `"10"`→10, `"10s"`→10,
355
+ `"01:02:03"`→3723, `"01:02"`→62, `"1,30"`→31, `"1'30''"`→31,
356
+ `"-10s"`→-10, `"1d"`→86400, `"1mo"`→2678400, `"1y"`→31536000.
357
+ Docs claiming `"1h30m"`-style compound parsing or lenient unknown-unit
358
+ handling are wrong.
359
+
360
+ ### 5.2 misc/digest.rb (93 lines) — content digests
361
+ - `MAX_ARRAY_DIGEST_LENGTH = 100_000` (`:2`).
362
+ - `digest_str(obj)` (`:4-51`) — recursive stable string; Strings that look
363
+ like existing files are digested as paths with a memo cache (`:10-14`,
364
+ `@@digest_str_cache`); Integers/Symbols `to_s`; huge Arrays (>100k) sampled
365
+ at positions 1,2,mid,len-2,len-1 (`:19-26`); Floats formatted by magnitude
366
+ (`:32-40`); Procs → digest of `source_location` (`:45-46`); else `inspect`.
367
+ - `digest(obj)` (`:53-56`) — MD5 hex of `digest_str` (Strings used verbatim).
368
+ - `file_md5(file)` (`:58-66`) — `Digest::MD5.file`; on failure falls back to
369
+ hashing the *path string* (`:63-65`) — intentional but surprising.
370
+ - `fast_file_md5(file, sample = 3_000_000)` (`:68-79`) — MD5 of
371
+ `size:` + first/middle/last 3MB samples; note `f.seek(size - sample - 1)`
372
+ (`:75`) can go negative for small files (seek to negative offset) — in
373
+ practice guarded by callers comparing `File.size(file) > 10_000_000`
374
+ (`:87`).
375
+ - `digest_file(file)` (`:81-92`) — honours a sibling `<file>.md5` file
376
+ (`:84-86`); >10MB → fast_file_md5 else file_md5 (`:87-91`).
377
+
378
+ ### 5.3 misc/filesystem.rb (86 lines)
379
+ - `in_dir(dir)` (`:2-11`) — mkdir_p + chdir with ensure-restore.
380
+ - `path_relative_to(basedir, path)` (`:13-24`).
381
+ - `tarize_cmd(path, dest)` (`:26-34`) — shells out to `tar cvfz` via CMD.
382
+ - `tarize(source_dir, archive_path)` (`:36-73`) — pure-Ruby tar.gz writer
383
+ using `Gem::Package::TarWriter` + `Zlib::GzipWriter`.
384
+ - `untar(file, target)` (`:75-85`) — `tar xvfz` via CMD.
385
+
386
+ ### 5.4 misc/helper.rb (78 lines) — array helpers
387
+ - `intersect_sorted_arrays(a1, a2)` (`:2-19`) — **destructive on inputs**
388
+ (uses `shift`).
389
+ - `counts(array)` (`:21-29`) — Hash tally. Duplicated verbatim in math.rb:75-83.
390
+ - `chunk(array, size)` (`:34-49`), `divide(array, num)` (`:53-62`, round-robin
391
+ distribution), `ordered_divide(array, num)` (`:66-76`, contiguous slices).
392
+ Probes/test: `chunk(%w(1..9),2)[0]`==%w(1 2); `ordered_divide(...,2).length`==5.
393
+
394
+ ### 5.5 misc/hook.rb (50 lines) — top-level `Hook`
395
+ - `Hook.extended(hook_class)` (`:2-4`), `Hook.apply(hook_class, base_class)`
396
+ (`:6-43`) — wraps singleton and instance methods of `base_class` so each
397
+ registered hook gets a chance (`claim` predicate opt-in), falling back to
398
+ `orig_<method>`; `Hook.hook_method` (`:45-49`).
399
+ - Not required by `misc.rb`; loaded only when explicitly required
400
+ (`test/scout/misc/test_hook.rb` does `require 'scout/util/misc'`-style load
401
+ or direct).
402
+
403
+ ### 5.6 misc/insist.rb (56 lines) — retry block
404
+ - `insist(times = 4, sleep = nil, msg = nil)` (`:2-55`):
405
+ - `TryAgain` → sleep+retry unconditionally (no counter) (`:22-24`);
406
+ - `StopInsist` → re-raise the wrapped exception (`:25-26`);
407
+ - `Aborted`/`Interrupt` → warn and re-raise, no retry (`:27-33`);
408
+ - other `Exception` → warn (unless `msg == false`), backoff using either a
409
+ caller-supplied sleep, an Array of sleeps (`times` as Array, `:10-14`),
410
+ or a synthesized ladder `[0, 0.001, 0.01, 0.1, 0.5]` (`:17`), retry while
411
+ `try < times` (`:51-52`), else re-raise (`:53`).
412
+ - `SCOUT_LOG_INSIST=true` triggers `Log.exception` on each failure (`:35`).
413
+
414
+ ### 5.7 misc/matching.rb (47 lines)
415
+ - `_convert_match_condition(condition)` (`:2-11`) — `'true'`→true,
416
+ `'false'`→false, `/re/`→Regexp, `<=x`/`>=x`/`<x`/`>x`→`[:cmp, op, x.to_f]`,
417
+ `!x`→`[:invert, ...]`, else the raw String.
418
+ - `match_value(value, condition)` (`:13-42`) — nil/nil→true (`:16`), nil
419
+ value→false (`:17`); Regexp; TrueClass/FalseClass conditions accept the
420
+ class objects or the strings "true"/"false" (`:22-25`); String condition
421
+ numeric-compares when value is Numeric (`:27`); Array conditions handle
422
+ `:cmp`, `:invert`, and OR-of-conditions (`:30-38`); unknown → raise
423
+ (`:39-41`).
424
+ - `tokenize(str)` (`:44-46`) — `"`/`'`/bare tokens.
425
+
426
+ ### 5.8 misc/math.rb (121 lines)
427
+ - `log2`/`log10` with precomputed multipliers (`:3-11`), `max`/`min`
428
+ nil-skipping (`:13-29`), `std_num_vector` (`:31-38`), `sum` (`:40-42`),
429
+ `mean` (`:44-46`), `median` (`:48-52`), `variance` (sample, n-1, `:54-67`),
430
+ `sd` (`:69-73`), `counts` (duplicate of helper.rb's, `:75-83`),
431
+ `proportions(array)` (`:85-101`, with a singleton `to_s` override that
432
+ prints a sorted tally), `zscore` (`:103-107`), `softmax` (`:109-120`).
433
+
434
+ ### 5.9 misc/monitor.rb (67 lines)
435
+ - `pid_alive?(pid)` (`:2-5`), `benchmark(repeats, message)` (`:7-27`),
436
+ `profile(options)` (`:29-45`, requires `ruby-prof` at call time),
437
+ `exec_time(&block)` (`:47-56`), `wait_for_interrupt` (`:58-66`, sleeps until
438
+ Interrupt).
439
+
440
+ ### 5.10 misc/system.rb (118 lines)
441
+ - `add_libdir(dir)` (`:3-6`), `hostname` (`:8-12`, `ENV["HOSTNAME"]` or
442
+ backticks), `children(ppid)` (`:14-19`, `sys/proctable`), `wait_child(pid)`
443
+ (`:21-26`), `abort_child(pid, wait)` (`:28-35`, TERM),
444
+ `env_add(var, value, sep = ":", prepend = true)` (`:37-49`, idempotent;
445
+ test: appending `test_value1:test_value2`),
446
+ `with_env_hash`/`with_env`/`with_envs` (`:51-82`, ENV snapshot/restore),
447
+ `update_git(gem_name = 'scout-essentials')` (`:85-113`, git pull +
448
+ submodule update + rake install via `CMD.cmd_log`), `processors`
449
+ (`:115-117`, `Etc.nprocessors`).
450
+
451
+ ### Env vars read by Misc files
452
+ `SCOUT_NOCOLOR` (format.rb:28), `HOSTNAME` (system.rb:10),
453
+ `SCOUT_LOG_INSIST` (insist.rb:35).
454
+
455
+ ### Subtle points
456
+ - `Misc.counts` defined twice (helper.rb:21, math.rb:75) — identical.
457
+ - `intersect_sorted_arrays` mutates its arguments.
458
+ - `file_md5` fallback hashes the path string, not the content.
459
+ - `Hook` is top-level and not auto-required.
460
+ - `timespan` raises TypeError for unknown units *and* for compound strings
461
+ like `"1h30m"` (nil multiplier, P7/P15) — it only accepts a single
462
+ `(<digits>)(<unit>)` pair.
463
+
464
+ ---
465
+
466
+ ## 6. `TmpFile` — `lib/scout/tmpfile.rb` (131 lines)
467
+
468
+ ### Structure / conventions
469
+ - `MAX_FILE_LENGTH = 150` (`:7`).
470
+ - `user_tmp(subdir = nil)` (`:9-15`) — **`ENV["HOME"]/tmp/scout[/<subdir>]`**
471
+ (home-relative, not `/tmp`!). `tmpdir` defaults to
472
+ `user_tmp('tmpfiles')` (`:21-23`), overridable via `TmpFile.tmpdir=`.
473
+ - `random_name(prefix = 'tmp-', max = 1_000_000_000)` (`:27-30`),
474
+ `tmp_file(prefix, max, dir)` (`:33-37`, Path-aware).
475
+ - `with_file(content = nil, erase = true, options = {})` (`:39-75`) — flexible
476
+ arg shifts (Hash-only call, Hash-as-second-arg); options `:prefix`,
477
+ `:tmpdir`, `:max`, `:extension`; IO content streamed in 1024-byte chunks
478
+ (`:57-65`); file removed in all cases after the block when `erase`
479
+ (`:72`).
480
+ - `with_dir(erase = true, options = {})` (`:77-88`), `in_dir(*args)`
481
+ (`:90-96`, wraps `Misc.in_dir`).
482
+ - `SLASH_REPLACE = '·'` (`:98`) — **MIDDLE DOT U+00B7**, not a hyphen.
483
+ - `tmp_for_file(file, tmp_options = {}, other_options = {})` (`:99-130`):
484
+ - explicit `:file` in tmp_options short-circuits (`:100-101`);
485
+ - base name = `prefix + ":" + file` (or bare file), `.gz`/`.bgz` stripped
486
+ (`:103-107`), `[key]` appended for `:key` (`:109`);
487
+ - `other_options[:filters]` adds `&F[match=<digest>]` segments (`:111-115`);
488
+ - whitespace → `_`, `/` → `·` (`:120`);
489
+ - names longer than `MAX_FILE_LENGTH + 10` are truncated at 150 with the
490
+ MD5 of the tail appended (`:125`);
491
+ - non-empty remaining `other_options` (minus `:unnamed`, `:122-123`) append
492
+ `:<md5 of options>` (`:127`);
493
+ - result is a `Path` inside `persistence_dir` (`:117-129`).
494
+
495
+ ### Probes (P4)
496
+ - `user_tmp` → `/home/mvazque2/tmp/scout`; `user_tmp('foo')` →
497
+ `.../tmp/scout/foo`; `tmpdir` → `.../tmp/scout/tmpfiles`.
498
+ - `tmp_file('pfx-')` → `.../tmpfiles/pfx-332333330` (random).
499
+ - `random_name` → `"tmp-57604039"`.
500
+ - `tmp_for_file("/a/b/c.tsv")` →
501
+ `.../tmpfiles/·a·b·c.tsv` (slashes → middle dots).
502
+ - `tmp_for_file("/a/b/c.tsv", {}, {filters: {...}})` → 32-char md5 suffix
503
+ (P4/P19).
504
+ - `tmp_for_file("/a/b/c.tsv", {prefix: "P", key: "K"}, {unnamed: 1})` →
505
+ `.../tmpfiles/P:·a·b·c.tsv[K]` — note `:unnamed` excluded from the digest
506
+ suffix, and with only `:unnamed` present no `:`-digest is appended.
507
+
508
+ ### Conventions summary (doc-relevant)
509
+ - Temp root is `$HOME/tmp/scout`, subdir `tmpfiles`.
510
+ - Deterministic cache names use `·` for `/`, optional `PREFIX:` and `[key]`
511
+ decorations, optional `&F[...]` filter markers, and a trailing
512
+ `:md5` when extra options survive filtering.
513
+
514
+ ### Error handling
515
+ `with_file`/`with_dir` attempt removal **only in the success path** — an
516
+ exception propagating out of the block skips cleanup (`:70-74`: no `ensure`
517
+ around the `yield`; verified by probe P12, which observed the temp file
518
+ surviving a raised block). Docs claiming "always cleaned up" would be wrong.
519
+
520
+ ---
521
+
522
+ ## 7. Exceptions — `lib/scout/exceptions.rb` (39 lines)
523
+
524
+ All top-level constants (no `Scout::` namespace). Verified hierarchy (probe
525
+ P9):
526
+
527
+ | Constant | Superclass | Notes |
528
+ |---|---|---|
529
+ | `ScoutDeprecated` | StandardError | `:1` |
530
+ | `ScoutException` | StandardError | `:2` |
531
+ | `FieldNotFoundError` | StandardError | `:3` |
532
+ | `TryAgain` | StandardError | `:5` |
533
+ | `StopInsist` | **Exception** | `:6-11`, wraps `#exception` accessor |
534
+ | `Aborted` | StandardError | `:13` |
535
+ | `ParameterException` | ScoutException | `:15` |
536
+ | `MissingParameterException` | ParameterException | `:16-20`, msg "Missing parameter 'x'" |
537
+ | `ProcessFailed` | StandardError | `:21-36`, `pid`,`msg` accessors; `new(nil,msg)` → "Failed to run msg" |
538
+ | `ConcurrentStreamProcessFailed` | ProcessFailed | `:37-43`, `#concurrent_stream` = filename if available |
539
+ | `OpenURLError` | StandardError | `:45` |
540
+ | `DontClose` | **Exception** | `:47-53`, `#payload` |
541
+ | `DontPersist` | **Exception** | `:55` |
542
+ | `KeepLocked` | DontPersist | `:56-61`, `#payload` |
543
+ | `KeepBar` | **Exception** | `:63-68`, `#payload` |
544
+ | `LockInterrupted` | TryAgain | `:70` |
545
+ | `ClosedStream` | StandardError | `:72` |
546
+ | `ResourceNotFound` | ScoutException | `:74` |
547
+ | `CMD::Timeout` | ProcessFailed | cmd.rb:22-29 (see §11) |
548
+
549
+ Probes: `ProcessFailed.new(123,"msg").message` → "Process 123 failed - msg";
550
+ `ProcessFailed.new(nil,"cmd").message` → "Failed to run cmd";
551
+ `StopInsist.new(ArgumentError.new("x")).exception` → the ArgumentError.
552
+
553
+ Key design fact: several control-flow signals (`StopInsist`, `DontClose`,
554
+ `DontPersist`, `KeepLocked`, `KeepBar`) derive from **Exception, not
555
+ StandardError**, so a blanket `rescue => e` (which catches StandardError)
556
+ will *not* intercept them. This is deliberate and a classic doc-trap.
557
+
558
+ `ScoutDeprecated` and `FieldNotFoundError` appear unused within this chunk
559
+ (search of lib/ found `module Scout` only in config.rb / resource; no other
560
+ references inspected here) — flag for the doc-writer to verify usage in
561
+ downstream repos.
562
+
563
+ ---
564
+
565
+ ## 8. `Log` — `lib/scout/log.rb` (453) + `log/{color,color_class,fingerprint,trap}.rb`
566
+
567
+ ### 8.1 log.rb core
568
+ - Severity constants and names (`:14-20`): `DEBUG LOW MEDIUM HIGH INFO WARN
569
+ ERROR NONE` = 0..7. `SEVERITY_NAMES` frozen via `||=`.
570
+ - `default_severity` (`:21-33`) — reads `$HOME/.scout/etc/log_severity`, else
571
+ `INFO` (4). Memoized in `@@default_severity`.
572
+ - ENV `SCOUT_LOG` maps names to severities; unset or unrecognized →
573
+ default_severity (`:35-54`).
574
+ - `tty_size` (`:56-72`) — `IO.console.winsize.last`, fallback `tput cols`,
575
+ fallback `ENV["TTY_SIZE"]` or 80; wrapped in `ignore_stderr`; memoized.
576
+ - `last_caller(stack)` (`:75-83`) — first frame not from `scout/log.rb`.
577
+ - `get_level(level)` (`:85-98`) — Numeric → int, String/Symbol → const, else
578
+ 0; on bad name calls `Log.exception` (which returns nil → `|| 0`).
579
+ - `with_severity(level)` (`:100-108`).
580
+ - `logfile(file=nil)` (`:110-124`) — nil resets, String opens append+sync,
581
+ IO accepted, else raise. Note `Log.logfile` with no args *resets* rather
582
+ than reads — trap.rb:7 relies on this returning the old value? No: `:7`
583
+ calls `Log.logfile` (no args) then restores with `Log.logfile = old_logfile`
584
+ via the attr_writer — actually `trap.rb:7,36` reads then re-assigns; the
585
+ attr_writer is declared at `:11` (`attr_writer :tty_size, :logfile`).
586
+ - `up_lines/down_lines/return_line/clear_line` (`:125-139`) — ANSI cursor
587
+ movement, all no-ops under `nocolor`.
588
+ - `MUTEX`-synchronized `log_write`/`log_puts` (`:141-166`) — write to
589
+ `@@logfile` if set else STDERR; IOError swallowed.
590
+ - `logn(message, severity = MEDIUM)` (`:169-190`) — prefix
591
+ `MM/DD/YY-HH:MM:SS.mmm[SEVERITY]`; `[pid]` included when
592
+ `SCOUT_DEBUG_PID=true` (`:178-182`); messages at severity >= INFO are
593
+ highlighted (`:183`); updates `Log::LAST` to "log" (`:188`) — the shared
594
+ string used for interleaving control with progress bars.
595
+ - `log(message, severity = MEDIUM, &block)` (`:192-198`) — appends "\n",
596
+ lazy block evaluation.
597
+ - `log_obj_inspect` / `log_obj_fingerprint` (`:200-224`).
598
+ - Severity helpers `debug/low/medium/high/info/warn/error` (`:226-252`).
599
+ - `exception(e)` (`:254-268`) — messages containing "NOLOG" are dropped
600
+ (`:255`); "NOSTACK" suppresses backtrace (`:260`); default prints reversed
601
+ (innermost-first) backtrace unless `SCOUT_ORIGINAL_STACK=true` (`:261-267`).
602
+ - `deprecated(m)` (`:270-274`), `color_stack` (`:276-288`, colorizes
603
+ workflow/scout-/rbbt- frames), `tsv(tsv, example)` (`:290-316`),
604
+ `stack(stack)` (`:318-330`), `count_stack`/`with_stack_counts`
605
+ (`:332-355`).
606
+ - Kernel-level debug helpers defined at top level (`:358-451`): `ppp`, `fff`,
607
+ `ddd/lll/mmm/iii/wwww/eee` (inspect at each severity), `ddf/llf/mmf/iif/
608
+ wwwf/eef` (fingerprint variants), `sss(level,&block)` (severity switch),
609
+ `ccc(obj,&block)` (conditional on `$scout_debug_log`).
610
+
611
+ ### 8.2 log/color.rb (228 lines)
612
+ - `module Colorize` (`:6-128`): `colors` (name→hex IndiferentHash, `:11-19`),
613
+ `diverging_colors` (`:25-40`, 12 hex values), `from_name(color)`
614
+ (`:42-62`, hex passthrough, name lookup, special white/black/green/red/
615
+ yellow/blue remaps), `continuous(array, start, eend, percent)`
616
+ (`:64-80`), `gradient`/`rank_gradient` (`:82-94`), `distinct(array)`
617
+ (`:97-112`, cycles diverging colors darkened by 0.3/times), `tsv(tsv,
618
+ options)` (`:114-127`).
619
+ - `module Log` extends `Term::ANSIColor` (`:130-131`); `nocolor` accessor
620
+ initialized from `ENV["SCOUT_NOCOLOR"] == 'true'` (`:137`).
621
+ - `WHITE, DARK, GREEN, YELLOW, RED = Color::SOLARIZED.values_at :base0,
622
+ :base00, :green, :yellow, :magenta` (`:139`) — note GREEN maps to
623
+ SOLARIZED[:green] but YELLOW maps to :magenta and RED to :magenta too
624
+ (last assignment wins for the tuple order base0, base00, green, yellow,
625
+ magenta → WHITE, DARK, GREEN, YELLOW, RED). Actually `values_at` returns 5
626
+ values; YELLOW←:yellow, RED←:magenta per the source order `:base0, :base00,
627
+ :green, :yellow, :magenta`.
628
+ - `SEVERITY_COLOR` (`:141`) = `[reset, cyan, green, magenta, blue, yellow,
629
+ red]` indexed by severity int.
630
+ - `CONCEPT_COLORS` (`:142-162`) — named concepts (title, path, value, error,
631
+ done, started, ...).
632
+ - `HIGHLIGHT = "\033[1m"` (`:163`).
633
+ - `uncolor(str)` (`:165-167`), `reset_color` (`:169-171`).
634
+ - `color(color, str = nil, reset = false)` (`:173-216`) — returns plain dup
635
+ under nocolor; special-cases `:integer`/`:float` (sign/magnitude coloring,
636
+ `:176-184`) and `:status` (`:186-201`); Integer color indexes
637
+ SEVERITY_COLOR; concept names resolve via CONCEPT_COLORS; other Symbols via
638
+ `Term::ANSIColor`; nil str → just the color string.
639
+ - `highlight(str = nil)` (`:218-226`).
640
+
641
+ ### 8.3 log/color_class.rb (269 lines)
642
+ - Vendored `class Color` (McClain Looney, 2007, MIT) with `SOLARIZED` palette
643
+ (`:33-52`), rgba accessors (`:90-93`), `Color.parse` (`:96`),
644
+ `lighten/darken/blend` (`:161-234`), hex conversion helpers, and a
645
+ top-level `rgb(*args)` convenience (`:265`).
646
+
647
+ ### 8.4 log/fingerprint.rb (82 lines)
648
+ - `FP_MAX_STRING = 150`, `FP_MAX_ARRAY = 20`, `FP_MAX_HASH = 10` (`:3-5`).
649
+ - `truncate_string(string, max)` (`:7-16`) — keeps head/tail around
650
+ `<...length - md5[0..4]...>`.
651
+ - `fingerprint(obj)` (`:18-81`) — dispatches to `obj.fingerprint` when
652
+ available (`:19`); nil/true/false/Symbol; Strings quoted with `'` and
653
+ newlines escaped (`:30-32`); ConcurrentStream (`:33-36`); IO/File
654
+ (`:37-40`); Arrays truncated to first/2nd/mid/last-2/last with length
655
+ prefix (`:41-46`); Hashes >10 entries collapse to `H:{keys;values}`
656
+ (`:47-61`); Floats by magnitude — `>10`→`%.1f`, `>1`→`%.3f`, else
657
+ `%.6f` (`:62-69`); Thread → `thread["name"]`; Set → array; else `to_s`.
658
+ - Probes (P8): `Log.fingerprint("a"*250)` → `'aaa...<...250 -
659
+ 63c7c...>...aaa'`; `[1,2]`→`"[1, 2]"`; `{:a=>1}`→`"{:a=>1}"`;
660
+ `3.5`→`"3.500"`; `100.0`→`"100.0"`; `2.5`→`"2.500"`; `0.000123`→
661
+ `"0.000123"`; nil/true/:sym as expected.
662
+
663
+ ### 8.5 log/trap.rb (107 lines)
664
+ - `trap_std(msg = "STDOUT", msge = "STDERR", severity = 0, severity_err =
665
+ nil)` (`:2-38`) — replaces STDOUT/STDERR with pipes, spawns two reader
666
+ threads that `Log.logn` each line, redirects Log's own output to the saved
667
+ STDERR dup, restores in ensure and joins threads.
668
+ - `trap_stderr(msg = "STDERR", severity = 0)` (`:40-62`).
669
+ - `_ignore_stderr`/`ignore_stderr` (`:64-84`) and
670
+ `_ignore_stdout`/`ignore_stdout` (`:86-106`) — reopen to /dev/null with
671
+ restore; fall back to plain yield if /dev/null is missing.
672
+
673
+ ### Env vars read
674
+ `HOME` (log.rb:23), `SCOUT_LOG` (:35), `TTY_SIZE` (:64,66), `SCOUT_DEBUG_PID`
675
+ (:178), `SCOUT_ORIGINAL_STACK` (:261,319), `SCOUT_NOCOLOR` (color.rb:137).
676
+
677
+ ### Subtle points
678
+ - `Log.logfile` with no argument **resets** the logfile (log.rb:111-113); it
679
+ is not a reader. `trap.rb:7,43` therefore capture/restore semantics rely on
680
+ the writer (`Log.logfile = ...`).
681
+ - Default severity is INFO (4): DEBUG/LOW/MEDIUM/HIGH messages are **not**
682
+ printed by default (P11).
683
+ - The reversed-backtrace default in `Log.exception` and `Log.stack`.
684
+ - `Log::LAST` is a single shared mutable String used as a protocol flag
685
+ between `logn` and progress-bar printing.
686
+
687
+ ---
688
+
689
+ ## 9. `Log::ProgressBar` — `log/progress.rb` (106) + `progress/{util,report}.rb`
690
+
691
+ ### 9.1 progress.rb
692
+ - `Log.no_bar=`/`no_bar` (`:5-12`) — class var plus
693
+ `ENV["SCOUT_NO_PROGRESS"] == "true"`.
694
+ - `ProgressBar` (`:14-105`): class attrs `default_file`, `default_severity`
695
+ (`:16-19`); instance attrs `max ticks frequency depth desc file bytes
696
+ process callback severity` (`:21`).
697
+ - `initialize(max = nil, options = {})` (`:23-43`) — options pulled with
698
+ `IndiferentHash.process_options` (destructive), defaults
699
+ `:depth => 0, :frequency => 2, :severity => default_severity`; `max = nil`
700
+ when `TrueClass === max` (`:28`); desc newlines stripped (`:38`).
701
+ - `percent` (`:45-49`) — 0 when no ticks, **100 when max == 0** (`:47`).
702
+ - `init` (`:55-62`), `tick(step = 1)` (`:64-86`) — no-op under `no_bar`;
703
+ reports when `diff >= frequency` or when the percent advanced and
704
+ `diff > 0.3` (`:77,85`).
705
+ - `pos(pos)` (`:88-91`), `process(elem)` (`:93-104`) — the `process` callback
706
+ may return false (ignore), true (tick), Integer (pos) or Float (fraction
707
+ of max).
708
+
709
+ ### 9.2 progress/util.rb (173 lines)
710
+ - `BAR_MUTEX`, `BARS`, `REMOVE`, `SILENCED` (`:4-7`).
711
+ - `add_offset`/`remove_offset`/`offset` (`:9-28`) — nesting offset for bars
712
+ created by threads.
713
+ - `new_bar(max, options = {})` (`:30-40`) — Hash-only call supported
714
+ (`:31-32`); `cleanup_bars` first; depth default = `BARS.length + offset`.
715
+ - `cleanup_bars` (`:42-63`), `remove_bar(bar, error = false)` (`:65-79`,
716
+ calls `bar.error`/`bar.done`), `remove(error)` instance wrapper (`:81-83`).
717
+ - `with_bar(max = nil, options = {})` (`:85-99`) — honors `options[:bar]`;
718
+ rescues `KeepBar` to keep the bar, any other exception marks error and
719
+ re-raises.
720
+ - `guess_obj_max(obj)` (`:101-137`) — `wc -l` via CMD for Step paths and
721
+ files (nil for gzip/bgzip/remote), `length`/`size` for TSV/Array/Hash.
722
+ - `get_obj_bar(obj, bar = nil)` (`:139-165`) — String→desc, true→auto max,
723
+ Numeric→explicit max, Hash→options with `:max`, ProgressBar→reused (max
724
+ filled in), Step→desc+file.
725
+ - `with_obj_bar(obj, bar = true)` (`:167-170`).
726
+
727
+ ### 9.3 progress/report.rb (244 lines)
728
+ - `print(io, str)` (`:4-9`) — gated by bar severity vs `Log.severity` and
729
+ `Log.no_bar`; writes via `Log.log_write`; sets `Log::LAST` = "progress".
730
+ - `thr_msg` (`:12-85`) — throughput estimate from a bounded history window
731
+ (max 30 samples, growth heuristics `:18-35`); mean/mean_max tracked
732
+ (`:41-53`); formats "N per sec." or "X secs each".
733
+ - `eta_msg` (`:88-117`) — 10-dot indicator, percent, `HH:MM:SS` ETA and used
734
+ time, ticks of max, bytes/items.
735
+ - `report_msg` (`:119-136`), `load(info)` (`:138-159`), `save` (`:161-166`,
736
+ YAML to `file`), `report(io = STDERR)` (`:168-197`) — redraws the stack of
737
+ active bars using cursor up/down lines, appends itself to BARS, saves to
738
+ file.
739
+ - `done(io = STDERR)` (`:199-218`) — prints summary, removes the YAML file,
740
+ invokes `callback`.
741
+ - `error(io = STDERR)` (`:220-242`) — same but red, and callback failures are
742
+ swallowed with a debug log.
743
+
744
+ ### Concurrency
745
+ Single `BAR_MUTEX` guards BARS/REMOVE/SILENCED; per-bar state (`ticks`,
746
+ history) is **not** synchronized — `tick` from multiple threads races by
747
+ design. `with_bar` handles KeepBar.
748
+
749
+ ### Env vars
750
+ `SCOUT_NO_PROGRESS` (progress.rb:11).
751
+
752
+ ### Subtle points
753
+ - Bars persist state to YAML in `file` (progress report.rb:161-166) and are
754
+ removed on done/error — a doc claiming progress state is ephemeral would be
755
+ wrong for filed bars.
756
+ - `percent` returns 100 when `max == 0` regardless of ticks.
757
+ - `Log.no_bar` short-circuits `tick` entirely (no bookkeeping at all).
758
+
759
+ ---
760
+
761
+ ## 10. `SOPT` — `lib/scout/simple_opt.rb` (5 lines) + 5 submodules
762
+
763
+ ### 10.1 accessor.rb (54 lines)
764
+ - Module-level state: `all`, `shortcuts`, `inputs`, `input_shortcuts`,
765
+ `input_types`, `input_descriptions`, `input_defaults` (`:6-32`), writers
766
+ (`:3`), `reset` (`:34-37`), `delete_inputs(inputs)` (`:39-48`),
767
+ `usage` (`:50-53`, prints doc and `exit 0`).
768
+
769
+ ### 10.2 parse.rb (69 lines)
770
+ - `fix_shortcut(short, long)` (`:2-35`) — collision resolution: keep the
771
+ requested short unless taken (`:3`); if the long name already owns a
772
+ shortcut, reuse it (`:5-6`); else derive from initials (`:13-16`) or digit
773
+ (`:17-20`), then extend letter by letter skipping `.-_` (`:24-30`); returns
774
+ nil when unresolvable.
775
+ - `register(short, long, asterisk, description)` (`:37-46`) — the presence of
776
+ an asterisk means `:string`, otherwise `:boolean` (`:45`).
777
+ - `parse(opt_str)` (`:48-68`) — splits on newlines **or colons** (`:51-55`);
778
+ each entry `"-s--long[*] description"` parsed with the regex at `:61`.
779
+ Returns the list of long names.
780
+
781
+ ### 10.3 get.rb (59 lines)
782
+ - `GOT_OPTIONS` module constant IndiferentHash (`:2`).
783
+ - `consume(args = ARGV)` (`:6-47`) — walks args; stops at `--` (`:11`);
784
+ matches `--?(key)(=value)?`; unknown options are **skipped in place**
785
+ (`:18-20`); recognized ones are deleted from the array (`:22`);
786
+ `:string` inputs take the next arg as value (`:29-31`); booleans accept
787
+ `=false`/`F`/`FALSE`/`no` (with a warning when passed as a separate token,
788
+ `:33-36`) and otherwise evaluate true (`:37`); result keys are symbolized
789
+ with `keys_to_sym!` (`:42`) and merged into `GOT_OPTIONS` (`:44`).
790
+ - `get(opt_str)` (`:49-52`) = parse + consume(ARGV).
791
+ - `require(options, *parameters)` (`:54-58`) — raises `ParameterException`
792
+ for nil values (note: a `false` value passes).
793
+
794
+ ### 10.4 doc.rb (126 lines)
795
+ - `command` (`:8-10`, `File.basename($0)`), `summary`, `synopsys`
796
+ (deliberate misspelling of synopsis, `:16-23`), `description`.
797
+ - `input_format(name, type, default, short)` (`:29-47`) — renders
798
+ `-s,--name` plus type-specific suffixes (`[=false]`, `=<file|->` for
799
+ :tsv/:text, `=<list|file|->` for :array, `=<type>` otherwise) and
800
+ `(default: ...)`.
801
+ - `input_array_doc(input_array)` (`:49-70`), `input_doc(...)` (`:72-101`)
802
+ both auto-register options via `register`.
803
+ - `doc` (`:104-125`) — man-page-ish rendering with SYNOPSYS/DESCRIPTION/
804
+ OPTIONS sections.
805
+
806
+ ### 10.5 setup.rb (26 lines)
807
+ - `setup(str)` (`:3-25`) — splits the doc string on blank lines; first part
808
+ = summary (unless it starts with `$-`), next `$`-prefixed part = synopsys
809
+ (`$ ` prefix stripped, `:17`), following non-`-` parts = description, the
810
+ rest of `-` lines = options; then `SOPT.parse` and `SOPT.consume`.
811
+
812
+ ### Probes (P10)
813
+ - `SOPT.parse("-f--first* first arg:-f--fun")` → inputs `["first","fun"]`,
814
+ shortcuts `{"f"=>"first","fu"=>"fun"}` (note `f` collision resolved to
815
+ `fu`), types `{"first"=>:string,"fun"=>:boolean}`, descriptions
816
+ `{"first"=>"first arg","fun"=>""}`.
817
+ - `SOPT.consume(["-f","myfile","--fun"])` → `{:first=>"myfile",
818
+ :fun=>true}`; consuming again with extra unknown options
819
+ (`-x --other val`) leaves them in the args and merges
820
+ `{:flag=>true,:other=>"val"}` when registered.
821
+ - `SOPT.consume` stops at `--` and leaves `["--","positional"]` untouched
822
+ (P10).
823
+ - `fix_shortcut("f","fun")` → "fu".
824
+ - `parse` with `:` vs `\n` separators both work (P10: newline-separated
825
+ entries parsed identically).
826
+
827
+ ### Subtle points
828
+ - Boolean false values are recognized only via `=F`/`=false`/`=FALSE`/`=no`
829
+ (get.rb:37); a *separate* following token from that same set is consumed
830
+ with a warning (get.rb:33-36) — but note the truthiness shortcut means
831
+ `--flag anything` (a non-false next token) is NOT consumed, so a stray
832
+ positional after a boolean flag silently stays in args.
833
+ - `SOPT.require` treats `false` as present.
834
+ - `synopsys` is the actual method name (misspelled) — docs must use it.
835
+ - `consume` mutates the passed array (deletes recognized args).
836
+
837
+ ---
838
+
839
+ ## 11. `CMD` — `lib/scout/cmd.rb` (666 lines)
840
+
841
+ ### Structure
842
+ - `require`s: indiferent_hash, concurrent_stream, log, exceptions,
843
+ open/stream, stringio, open3, fileutils (`:1-8`).
844
+ - `TIMEOUT_KILL_GRACE = 1.0` (`:14`).
845
+ - `CMD::Timeout < ProcessFailed` (`:22-29`) — carries `command`, `timeout`;
846
+ message "command 'X' exceeded timeout of N seconds".
847
+ - `TOOLS` IndiferentHash (`:31`).
848
+
849
+ ### Tool management
850
+ - `tool(tool, claim = nil, test = nil, cmd = nil, &block)` (`:32-34`) —
851
+ registers `[claim, test, block, cmd]`.
852
+ - `conda(tool, env, channel = 'bioconda')` (`:36-42`).
853
+ - `get_tool(tool)` (`:45-89`) — if a test or `command -v` fails, installs via
854
+ `claim.produce`, a block returning a Hash (→ `Resource.install`), then
855
+ probes `--version`/`-version`/`--help`/no-flag to cache a version string in
856
+ `@@init_cmd_tool`.
857
+ - `scan_version_text(text, cmd)` (`:91-109`) — several regex heuristics for
858
+ version strings.
859
+ - `versions` (`:110-113`).
860
+ - `bash(cmd)` (`:115-118`) — wraps in `bash -l` heredoc with `:autojoin`.
861
+
862
+ ### Option processing
863
+ - `process_cmd_options(options = {})` (`:120-146`) — validates option keys
864
+ against `^[a-z_0-9\-=.]+$/i` (raises otherwise, `:125`), escapes single
865
+ quotes in values (`:127`), `:add_option_dashes` prefixes `--`, booleans
866
+ render as bare flags, nil/false are dropped, `x=` keys render `x='value'`.
867
+ Probe: `{"o1"=>"v1",:o2=>true,:o3=>false,"o4="=>"v"}` →
868
+ `"o1 'v1' o2 o4='v'"`; with dashes → `"--a '1' --long_opt 'x'"`. A key with
869
+ an apostrophe raises RuntimeError.
870
+ - `process_cmd_options_array(options)` (`:151-176`) — same semantics
871
+ returning an argv array (no quoting), used for the no-shell array mode.
872
+
873
+ ### `cmd(tool, cmd = nil, options = {}, &block)` (`:178-611`)
874
+ - Normalizes the `(Hash)` second-arg form (`:179`).
875
+ - Defaults `:stderr => Log::DEBUG` (`:181`); pops `:in :stderr :sudo :post
876
+ :pipe :log :no_fail/:nofail :no_wait :xvfb :progress_bar :save_stderr
877
+ :autojoin :timeout :dont_close_in` (`:182-222`).
878
+ - `:save_stderr` accepts true (accumulate in `out.std_err`), a path
879
+ (String/Pathname/Path — opened 'w', mkdir_p of dirname, closed by CMD) or
880
+ any `write`/`<<` object (never closed) (`:195-216`).
881
+ - `array_mode = Array === tool` (`:226`) — argv execution via
882
+ `Open3.popen3(ENV, *cmd_array)` (`:288-293`), no shell; otherwise a
883
+ single shell string via `get_tool` resolution and `process_cmd_options`
884
+ (`:249-284`). `'{opt}'` placeholder substitution (`:275-279`).
885
+ - `:xvfb` wraps with `xvfb-run` (`:232-237, 262-268`).
886
+ - `:sudo` prefixes sudo (`:245, 281-283`).
887
+ - Spawn failure → warn, close any opened stderr file, `raise ProcessFailed,
888
+ nil, cmd unless no_fail` (or `return`) (`:294-301`).
889
+ - `:timeout` (Numeric > 0) → watchdog thread (`:310-408`): waits
890
+ `wait_thr.join(timeout)`, on expiry sets `timed_out`, `Log.low`, then for
891
+ pipe mode `sout.abort(timeout_exception)` + reap, else
892
+ `caller_thread.raise`; reaping escalates INT → KILL after
893
+ `TIMEOUT_KILL_GRACE` and always waits the pid (`:341-370`). Watchdog is
894
+ not registered in `sout.threads`.
895
+ - stdin handling thread when `:in` responds to `read` (`:410-440`), else
896
+ `sin.close`.
897
+ - **pipe mode** (`:444-497`): `ConcurrentStream.setup sout` with
898
+ `:pids/:autojoin/:no_fail`; `:post` becomes `sout.callback`; an err thread
899
+ feeds `bar.process(line)` (progress), `sout.log` (last line),
900
+ `sout.std_err` accumulation, the save_stderr destination, and `Log.log` at
901
+ the configured severity; threads `[in_thread, err_thread, wait_thr]`
902
+ attached; returns the stream.
903
+ - **non-pipe mode** (`:498-610`): reads stdout into a StringIO, waits the
904
+ status, joins the watchdog, raises CMD::Timeout if timed out, annotates the
905
+ output with `exit_status`, fills `std_err`, writes the accumulated stderr
906
+ to the destination, raises `ProcessFailed` (message includes the captured
907
+ stderr) on non-success unless `no_fail`, else logs stderr at the given
908
+ severity; Timeout path aborts the internal stream and re-raises; ensure
909
+ closes the destination when CMD owns it and runs `post`.
910
+
911
+ ### Convenience wrappers
912
+ - `cmd_pid(*args)` (`:613-659`) — forces `:pipe`, streams chars to STDERR
913
+ honouring a log level, feeds `progress_bar`, joins and removes the bar.
914
+ - `cmd_log(*args)` (`:661-664`) — `cmd_pid` + nil.
915
+
916
+ ### External integrations
917
+ `Open3.popen3`, `xvfb-run`, `sudo`, `tar` (from misc), `bash -l`, `command -v`,
918
+ conda (bioconda), Resource installation. Gems: none beyond stdlib here
919
+ (`term-ansicolor` arrives via log).
920
+
921
+ ### Concurrency
922
+ Threads: watchdog (timeout), stdin feeder, stderr drainer, `wait_thr`; all
923
+ named; `report_on_exception = false` on helpers; ConcurrentStream carries
924
+ `pids` and `threads` for join semantics.
925
+
926
+ ### Probes
927
+ - P14: `CMD.cmd(["echo","a","b"]).read` → `"a b\n"`; save_stderr to a path
928
+ captures `"E1\n"`; `process_cmd_options` quoting as above; invalid key
929
+ raises.
930
+ - `CMD.tool("echo", nil, nil, "echo")` + `get_tool` → `"echo"`.
931
+
932
+ ### Subtle points
933
+ - The **default** stderr severity is `Log::DEBUG` (`:181`), so stderr is only
934
+ echoed at DEBUG or when explicitly given a severity; `:stderr => true`
935
+ maps to `Log::HIGH` (`:239-241, 270-272`).
936
+ - `no_fail` also suppresses the spawn-time ProcessFailed (`:299-300`).
937
+ - `{opt}` placeholder must be quoted in the command string (`:275-276`).
938
+ - `:save_stderr` with a path **truncates** ('w') and creates parent dirs.
939
+ - Timeout in pipe mode surfaces through `ConcurrentStream#abort` semantics,
940
+ not a direct raise (documented in the comment block `:310-325`).
941
+
942
+ ---
943
+
944
+ ## 12. Packaging / root files (read for context)
945
+
946
+ - `VERSION` → `1.8.8` (single line).
947
+ - `Gemfile` → no runtime deps; dev group only (`shoulda`, `rdoc ~> 3.12`,
948
+ `bundler ~> 1.0`, `juwelier ~> 2.1.0`, `simplecov`).
949
+ - `scout-essentials.gemspec` → juwelier-generated (`:1-2`), `s.name`/:8,
950
+ `s.version = "1.8.8"`/:9; no `add_dependency` lines (runtime deps live in
951
+ the Rakefile block instead).
952
+ - `Rakefile` → `ENV["BRANCH"] = 'main'`/:3; Juwelier::Tasks block
953
+ `gem.add_runtime_dependency 'term-ansicolor' | 'yaml' | 'rake' | 'listen'`
954
+ (:18-21); Rake::TestTask pattern `test/**/test_*.rb` (:26-28) with
955
+ `test.libs << 'lib' << 'test'`.
956
+ - `lib/scout.rb` does **not exist** (only `lib/scout-essentials.rb` and the
957
+ `lib/scout/` tree), so `require 'scout'` is not a supported entry point.
958
+ - Runtime gem dependencies actually observable from the audited files:
959
+ `term-ansicolor` (lib/scout/log/color.rb:4), stdlib `io/console`
960
+ (lib/scout/log.rb:6),
961
+ `open3`, `stringio`, `fileutils`, `yaml`
962
+ (lib/scout/log/progress/report.rb:1).
963
+
964
+ ---
965
+
966
+ ## Files covered checklist (CORE chunk)
967
+
968
+ Read in full (line-anchored above):
969
+
970
+ - [x] lib/scout-essentials.rb
971
+ - [x] lib/scout/indiferent_hash.rb
972
+ - [x] lib/scout/indiferent_hash/case_insensitive.rb
973
+ - [x] lib/scout/indiferent_hash/options.rb
974
+ - [x] lib/scout/indiferent_hash/serialize.rb
975
+ - [x] lib/scout/named_array.rb
976
+ - [x] lib/scout/misc.rb
977
+ - [x] lib/scout/misc/digest.rb
978
+ - [x] lib/scout/misc/filesystem.rb
979
+ - [x] lib/scout/misc/format.rb
980
+ - [x] lib/scout/misc/helper.rb
981
+ - [x] lib/scout/misc/hook.rb
982
+ - [x] lib/scout/misc/insist.rb
983
+ - [x] lib/scout/misc/matching.rb
984
+ - [x] lib/scout/misc/math.rb
985
+ - [x] lib/scout/misc/monitor.rb
986
+ - [x] lib/scout/misc/system.rb
987
+ - [x] lib/scout/tmpfile.rb
988
+ - [x] lib/scout/exceptions.rb
989
+ - [x] lib/scout/config.rb
990
+ - [x] lib/scout/log.rb
991
+ - [x] lib/scout/log/color.rb
992
+ - [x] lib/scout/log/color_class.rb
993
+ - [x] lib/scout/log/fingerprint.rb
994
+ - [x] lib/scout/log/trap.rb
995
+ - [x] lib/scout/log/progress.rb
996
+ - [x] lib/scout/log/progress/report.rb
997
+ - [x] lib/scout/log/progress/util.rb
998
+ - [x] lib/scout/simple_opt.rb
999
+ - [x] lib/scout/simple_opt/accessor.rb
1000
+ - [x] lib/scout/simple_opt/doc.rb
1001
+ - [x] lib/scout/simple_opt/get.rb
1002
+ - [x] lib/scout/simple_opt/parse.rb
1003
+ - [x] lib/scout/simple_opt/setup.rb
1004
+ - [x] lib/scout/cmd.rb
1005
+
1006
+ Supporting files read:
1007
+
1008
+ - [x] Gemfile
1009
+ - [x] scout-essentials.gemspec
1010
+ - [x] Rakefile
1011
+ - [x] VERSION
1012
+ - [x] (lib/scout.rb — confirmed absent)
1013
+
1014
+ Tests consulted for semantics:
1015
+
1016
+ - [x] test/scout/test_config.rb
1017
+ - [x] test/scout/test_tmpfile.rb
1018
+ - [x] test/scout/test_indiferent_hash.rb
1019
+ - [x] test/scout/indiferent_hash/test_options.rb
1020
+ - [x] test/scout/indiferent_hash/test_case_insensitive.rb (listed, semantics via source)
1021
+ - [x] test/scout/test_named_array.rb
1022
+ - [x] test/scout/test_misc.rb (empty)
1023
+ - [x] test/scout/misc/{test_digest,test_helper,test_system,test_matching,test_math,test_filesystem,test_hook,test_insist}.rb
1024
+ - [x] test/scout/test_log.rb
1025
+ - [x] test/scout/log/{test_color,test_fingerprint,test_progress}.rb
1026
+ - [x] test/scout/simple_opt/{test_parse,test_get,test_doc,test_setup}.rb
1027
+ - [x] test/scout/test_cmd.rb
1028
+ - [x] test/scout/test_cmd_save_stderr.rb (listed)
1029
+ - [x] test/test_helper.rb