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,272 @@
1
+ # Investigation: Command Execution and Logging
2
+
3
+ **Status:** Non-normative investigation artifact. May be outdated.
4
+
5
+ ## Scope
6
+ CMD and Log.
7
+
8
+ ---
9
+
10
+ ## CMD
11
+
12
+ ### What it is
13
+ A unified wrapper for running external commands with:
14
+ - Tool discovery and auto-installation.
15
+ - Streaming (pipe) and blocking (read-all) modes.
16
+ - stdin piping.
17
+ - stderr logging, progress tracking, and capture.
18
+ - sudo, xvfb, pipe-chaining support.
19
+ - SSH through Open (for remote commands).
20
+ - Option-hash-to-flags conversion.
21
+
22
+ ### Core API
23
+ ```ruby
24
+ # Blocking: returns a StringIO-like result
25
+ result = CMD.cmd("ls -la")
26
+ puts result.read
27
+
28
+ # Streaming (pipe: true): returns a ConcurrentStream
29
+ stream = CMD.cmd("cat huge_file", :pipe => true)
30
+ stream.each { |line| ... }
31
+
32
+ # With tool discovery
33
+ CMD.cmd(:samtools, "view -bS aln.sam")
34
+ ```
35
+
36
+ ### `CMD.cmd(tool, cmd = nil, options = {})`
37
+ - `tool` — may be a Symbol registered via `CMD.tool` (auto-install + discovery)
38
+ or a plain String command.
39
+ - `cmd` — optional subcommand appended after the tool.
40
+ - `options` — IndiferentHash with special keys:
41
+ - `:in` — String/IO/StringIO piped to stdin.
42
+ - `:pipe` — if true, return a ConcurrentStream immediately.
43
+ - `:stderr` — severity level (Integer) or boolean.
44
+ - `:progress_bar` — Log::ProgressBar or options hash.
45
+ - `:sudo` — prepend `sudo`.
46
+ - `:xvfb` — run under xvfb-run.
47
+ - `:autojoin` — autojoin the stream on close.
48
+ - `:no_fail` / `:nofail` — don't raise on failure.
49
+ - `:log` — toggle logging (default true).
50
+ - `:save_stderr` — capture stderr text.
51
+ - `:pipe` — output piped to another command.
52
+ - `:post` — callback executed after command completion.
53
+ - Other keys are converted to command-line flags.
54
+
55
+ ### Tool system (`CMD.tool` / `CMD.get_tool`)
56
+ ```ruby
57
+ CMD.tool(:samtools, nil, nil, "samtools") { install_samtools }
58
+ ```
59
+ - `TOOLS` is a global registry: `{ tool => [claim, test, block, cmd] }`.
60
+ - `get_tool(tool)` checks if the tool exists (via `test` or `command -v`).
61
+ - If not found, it tries:
62
+ 1. `claim.produce` (if a Resource claim is given).
63
+ 2. `block.call` (if a block is given).
64
+ - After installation, version is detected via `scan_version_text`.
65
+ - Tools are cached in `@@init_cmd_tool` to avoid repeated checks.
66
+
67
+ ### Option-to-flags conversion (`process_cmd_options`)
68
+ ```ruby
69
+ CMD.cmd("grep", :pattern => "foo", :ignore_case => true)
70
+ # → grep --pattern 'foo' --ignore_case
71
+ ```
72
+ - Boolean `true` → flag without value.
73
+ - Boolean `false`/`nil` → flag omitted.
74
+ - String → flag with quoted value.
75
+ - `--key=value` → `--key 'value'`.
76
+ - `--key=` → `--key='value'` (equals form).
77
+ - `:add_option_dashes => true` → adds `--` prefix to keys.
78
+
79
+ ### SSH / remote
80
+ CMD itself does not handle SSH directly. Use `Open.ssh` or `Open.open`/`Open.read`
81
+ with remote paths (detected via `Open.remote?`). However, CMD commands may
82
+ include ssh as a string.
83
+
84
+ ### `CMD.bash`
85
+ ```ruby
86
+ CMD.bash(<<~SH)
87
+ set -e
88
+ source ~/.bashrc
89
+ conda activate myenv
90
+ do_stuff
91
+ SH
92
+ ```
93
+ Wraps the string in a login shell.
94
+
95
+ ### `CMD.cmd_log` / `CMD.cmd_pid`
96
+ Variants that tee stdout/stderr to `Log` in real time, useful for long-running
97
+ commands.
98
+
99
+ ---
100
+
101
+ ## Log
102
+
103
+ ### What it is
104
+ A thread-safe, severity-based logging system with colored output, progress
105
+ bars, exception formatting, and support for redirecting output to a file or
106
+ IO.
107
+
108
+ ### Severity levels
109
+ ```ruby
110
+ Log::DEBUG = 0
111
+ Log::LOW = 1
112
+ Log::MEDIUM = 2
113
+ Log::HIGH = 3
114
+ Log::INFO = 4
115
+ Log::WARN = 5
116
+ Log::ERROR = 6
117
+ Log::NONE = 7
118
+ ```
119
+ Messages are only emitted if `severity >= Log.severity`.
120
+
121
+ ### Configuration
122
+ - `Log.severity = Log::DEBUG` — set threshold (Integer).
123
+ - `SCOUT_LOG` environment variable — DEBUG, LOW, MEDIUM, HIGH, WARN, ERROR, NONE.
124
+ - `~/.scout/etc/log_severity` file — numeric severity.
125
+ - `Log.logfile(path)` — redirect all log output to a file.
126
+ - `Log.nocolor = true` — disable ANSI colors.
127
+
128
+ ### Log methods
129
+ ```ruby
130
+ Log.debug("Detailed info") # DEBUG
131
+ Log.low("Slightly important") # LOW
132
+ Log.medium("Medium importance") # MEDIUM
133
+ Log.high("Important") # HIGH
134
+ Log.info("User-facing info") # INFO
135
+ Log.warn("Warning") # WARN
136
+ Log.error("Error") # ERROR
137
+ Log.exception(e) # ERROR + backtrace
138
+ ```
139
+
140
+ ### Block form (lazy evaluation)
141
+ ```ruby
142
+ Log.debug { "Computed value: #{expensive_computation()}" }
143
+ ```
144
+ The block is only evaluated if the severity threshold is met. Use this for
145
+ expensive-to-compute log messages.
146
+
147
+ ### Thread-safety
148
+ - `MUTEX = Mutex.new` guards `log_write` and `log_puts`.
149
+ - Multiple threads logging simultaneously are safe.
150
+ - The ProgressBar system uses `BAR_MUTEX` for its own state.
151
+
152
+ ### Log::ProgressBar
153
+ A sophisticated multi-bar progress tracking system:
154
+
155
+ ```ruby
156
+ Log::ProgressBar.with_bar(1000, :desc => "Processing") do |bar|
157
+ 1000.times do |i|
158
+ # ... work ...
159
+ bar.tick
160
+ end
161
+ end
162
+ ```
163
+
164
+ Features:
165
+ - **Stacked bars** — multiple concurrent bars rendered with vertical stacking
166
+ using ANSI cursor movement (`up_lines`/`down_lines`).
167
+ - **Throughput estimation** — short-term and long-term rate, ETA calculation.
168
+ - **Auto max-guessing** — `with_obj_bar` infers the max from file size
169
+ (`wc -l`), TSV/Array/Hash length, etc.
170
+ - **Persistence** — bar state can be saved to/reloaded from a file.
171
+ - **Depth tracking** — nested bars track their position in the stack.
172
+ - **Silencing** — `SILENCED` array hides specific bars from rendering.
173
+ - **Callback chains** — `callback` procs executed on completion.
174
+
175
+ #### Key methods
176
+ - `Log::ProgressBar.with_bar(max, options) { |bar| ... }` — main entry point.
177
+ - `bar.tick(n = 1)` — increment by n.
178
+ - `bar.pos(n)` — set absolute position.
179
+ - `bar.process(elem)` — call `@process` callback, then tick based on return.
180
+ - `bar.done` — print completion summary.
181
+ - `bar.error` — print error summary.
182
+ - `bar.remove` / `remove_bar` — clean up bar from display.
183
+ - `with_obj_bar(obj, desc_or_max) { |bar| ... }` — auto-infer max from object.
184
+
185
+ #### Bar removal
186
+ - On normal completion: `bar.done` then `remove_bar`.
187
+ - On exception: `bar.error` then `remove_bar`.
188
+ - `KeepBar` exception — prevents removal for debugging.
189
+
190
+ ### Log::Color
191
+ ANSI color codes for severity levels and general use:
192
+ ```ruby
193
+ Log.color(:yellow, "warning text")
194
+ Log.color(Log::INFO, "informational")
195
+ ```
196
+ - `nocolor?` — checks if colors are disabled.
197
+ - Colors are defined in `log/color_class.rb` with a mapping from severity → ANSI code.
198
+ - `Log.uncolor(text)` — strips ANSI codes.
199
+
200
+ ### Log::Trap
201
+ ```ruby
202
+ Log::Trap.trap('SIGINT') { ... }
203
+ ```
204
+ Safely intercepts signals. Stores handlers for restore. Ensures progress bars
205
+ are cleaned up on signal.
206
+
207
+ ### Log::fingerprint
208
+ ```ruby
209
+ Log.fingerprint(obj)
210
+ ```
211
+ Produces a compact, human-readable representation of an object for logging:
212
+ - String → the string (truncated if long).
213
+ - Array → `[a, b, ...]` (truncated).
214
+ - Hash → `{k => v, ...}` (truncated).
215
+ - Path → the path string.
216
+ - Other → `inspect`.
217
+
218
+ ---
219
+
220
+ ## Cross-module interactions
221
+
222
+ - **CMD depends on Log** — for stderr logging, debug logging, and progress bars.
223
+ - **CMD depends on ConcurrentStream** — for stream lifecycle.
224
+ - **CMD depends on Open** — for remote operations and `consume_stream`.
225
+ - **Log depends on IndiferentHash** — `process_options` in ProgressBar.
226
+ - **Log::ProgressBar depends on CMD** — `guess_obj_max` uses `CMD.cmd("wc -l")`.
227
+ - **Log depends on Misc** — `format_seconds`, `fixutf8`.
228
+ - **All modules use Log** — it is the foundational logging layer.
229
+
230
+ ---
231
+
232
+ ## Gotchas and warnings
233
+
234
+ 1. **CMD.cmd with `:pipe => true` returns immediately** — the command may
235
+ still be running when you receive the stream. You must `join` the stream
236
+ to wait for completion.
237
+ 2. **CMD.cmd default stderr level is Log::DEBUG** — by default, stderr is
238
+ logged at DEBUG severity. Use `:stderr => Log::HIGH` to make it visible at
239
+ default severity.
240
+ 3. **CMD option keys are validated** — `process_cmd_options` raises if an
241
+ option key contains characters outside `[a-z_0-9\-=.]+`. This prevents
242
+ shell injection.
243
+ 4. **Log.severity is global** — `Log.severity = Log::DEBUG` affects all
244
+ threads. Use `Log.with_severity(level) { ... }` for scoped severity changes.
245
+ This is thread-safe via a thread-local pattern.
246
+ 5. **Log::ProgressBar bars are global** — the `BARS` array is class-level.
247
+ Nested calls add bars. Bars from different threads may interleave in
248
+ display.
249
+ 6. **Log.debug block form is not always lazy** — `Log.debug { "..." }`
250
+ evaluates the block only if severity >= DEBUG, but `Log.debug("..." )`
251
+ always evaluates the string. For expensive messages, use the block form.
252
+ 7. **CMD.cmd missing tool** — if a tool is not found and no install block is
253
+ given, the command will fail with a `ProcessFailed` exception. Check
254
+ `CMD.versions` for known tools.
255
+ 8. **Log.nocolor detection** — checks `ENV['SCOUT_NO_COLOR']`, output not a
256
+ tty, and `Log.nocolor` flag. Colors may be unexpectedly disabled in
257
+ pipelines.
258
+ 9. **CMD.cmd option escaping** — single quotes are escaped (`'` → `\'`), but
259
+ other shell metacharacters are not escaped by default. This is by design
260
+ (commands may need them), but means user-supplied option values must be
261
+ sanitized.
262
+ 10. **Log.exception NOLOG/NOSTACK** — exception messages containing "NOLOG"
263
+ or "NOSTACK" will skip logging or backtrace printing. This is an internal
264
+ convention, not documented.
265
+ 11. **CMD.cmd with no_fail:true and pipe:true** — the stream will have
266
+ `no_fail` set, meaning `join` won't raise on process failure. Check
267
+ `stream.exit_status` to verify success.
268
+ 12. `CMD.cmd("grep", :pattern => "foo")` generates `grep --pattern 'foo'`.
269
+ The dashes are auto-added via `:add_option_dashes`. To suppress, pass
270
+ `:add_option_dashes => false`. Check current default: it appears to be
271
+ falsy by default, so `CMD.cmd("grep", "--pattern" => "foo")` may be
272
+ needed.
@@ -0,0 +1,383 @@
1
+ # Investigation: Design Philosophy and Cross-Cutting Patterns
2
+
3
+ **Status:** Non-normative investigation artifact. May be outdated.
4
+
5
+ ## Scope
6
+ Cross-cutting design principles that make scout-essentials elegant and
7
+ expressive, identified by analyzing patterns across all modules.
8
+
9
+ ---
10
+
11
+ ## Principle 1: Annotate, don't wrap
12
+
13
+ **The single most important principle.** When you have a piece of data that
14
+ already has a natural Ruby type (String, Array, Hash, IO), you do not create
15
+ a wrapper class. Instead, you annotate it.
16
+
17
+ ```ruby
18
+ # GOOD — annotate an existing object
19
+ path = Path.setup("data/file.tsv") # path.class == String
20
+ gene = Gene.setup("BRCA1", :organism, "Hsa") # gene.class == String
21
+
22
+ # WRONG — create a wrapper class
23
+ class MyPath < String; end # breaks String API
24
+ class GeneWrapper; def initialize(g); @g = g; end; end # loses String-ness
25
+ ```
26
+
27
+ Annotation uses Ruby's singleton-class extension:
28
+ - `extend SomeModule` on the specific object instance.
29
+ - The object's class is unchanged.
30
+ - The annotation is removable via `Annotation.purge`.
31
+
32
+ This principle is applied to: Path (annotated Strings), NamedArray (annotated
33
+ Arrays), IndiferentHash (annotated Hashes), ConcurrentStream (annotated IOs).
34
+
35
+ ---
36
+
37
+ ## Principle 2: Module composition over inheritance
38
+
39
+ There are no deep class hierarchies. Behavior is composed via Ruby modules
40
+ mixed into objects at runtime.
41
+
42
+ ```ruby
43
+ # Path = String + Path annotations
44
+ module Path
45
+ extend Annotation # becomes an annotation module
46
+ annotation :pkgdir, :libdir, :path_maps, :map_order, :where, :original
47
+ end
48
+
49
+ path = Path.setup("data/file.tsv") # String + Path module
50
+ ```
51
+
52
+ Each concern is a module: Annotation, IndiferentHash, ConcurrentStream,
53
+ Resource, Path. They can be mixed into the same object without conflict.
54
+
55
+ ---
56
+
57
+ ## Principle 3: `setup` is the constructor
58
+
59
+ Classes are rarely instantiated with `.new`. Instead, `Module.setup(obj, ...)`
60
+ extends an existing object with the module and sets its annotations.
61
+
62
+ ```ruby
63
+ # Annotation modules provide a class-level `setup` method
64
+ Path.setup("data/file.tsv", "mypkg") # → annotated String
65
+ IndiferentHash.setup({a: 1}) # → annotated Hash
66
+ ConcurrentStream.setup(io, pids: [123]) # → annotated IO
67
+ Gene.setup("BRCA1", :organism, "Hsa") # → annotated String
68
+ ```
69
+
70
+ `setup` always returns the same object it received (after extension). It does
71
+ not clone unless the object is frozen. This means `setup` is idempotent and
72
+ non-destructive.
73
+
74
+ ---
75
+
76
+ ## Principle 4: `method_missing` as a fluent builder
77
+
78
+ When a module needs to provide a dynamic, open-ended API, `method_missing` is
79
+ used to build structures fluently.
80
+
81
+ ```ruby
82
+ # Path uses method_missing to build nested paths
83
+ path = Path.setup("data")
84
+ path.genes.tsv # → "data/genes/tsv" (annotated Path)
85
+ path.results["run1"] # → "data/results/run1"
86
+
87
+ # NamedArray uses method_missing for field accessors
88
+ arr = NamedArray.setup([1, 2, 3], [:a, :b, :c])
89
+ arr.a # → 1
90
+ arr.b # → 2
91
+ ```
92
+
93
+ The key insight: `method_missing` is not for error recovery, it's for
94
+ **building an API that mirrors the domain**. Path segments become method
95
+ names; field names become accessors.
96
+
97
+ ---
98
+
99
+ ## Principle 5: Conventions for resource discovery
100
+
101
+ The framework discovers resources by convention, not by explicit registration:
102
+
103
+ | Convention | Resolution |
104
+ |---|---|
105
+ | Path maps `{PKGDIR}`, `{LIBDIR}`, `{PWD}`, `{HOME}` | Automatically resolved |
106
+ | `Resource.claim(path, type, &block)` | Declares how to produce a path |
107
+ | `CMD.tool(:name) { ... }` | Registers tool auto-install |
108
+ | `Path#find` | Tries each map in `map_order` |
109
+ | `Resource#produce` | Triggers claim if file missing |
110
+
111
+ There are no plugin manifests or registration calls. Put a claim in the right
112
+ module and the framework finds it.
113
+
114
+ ---
115
+
116
+ ## Principle 6: The "produced on demand" pattern
117
+
118
+ Files are not pre-generated. They are produced lazily when first accessed:
119
+
120
+ ```ruby
121
+ module MyData
122
+ extend Resource
123
+ self.claim Path.setup("data/file.tsv"), :proc do |path|
124
+ Open.write(path, generate_data())
125
+ end
126
+ end
127
+
128
+ path = MyData.data["file.tsv"] # accesses via method_missing
129
+ path.read # triggers produce → generate_data()
130
+ path.read # second read: file exists, no production
131
+ ```
132
+
133
+ This is the Scout equivalent of a build system. The claim is the build rule;
134
+ `produce` is the build trigger; `find` is the output path.
135
+
136
+ ---
137
+
138
+ ## Principle 7: Stream everything
139
+
140
+ Expensive I/O (command output, file reads, network) is always returned as
141
+ streams, not eagerly-read buffers. Streams are ConcurrentStream-enhanced IOs
142
+ with lifecycle management:
143
+
144
+ ```ruby
145
+ stream = CMD.cmd("cat huge_file", :pipe => true)
146
+ stream.each_line { |line| process(line) }
147
+ stream.join # wait for command completion
148
+ ```
149
+
150
+ - `pipe: true` → streaming mode.
151
+ - Default → blocking mode (reads all output).
152
+ - Streams carry their PID/threads for proper cleanup.
153
+ - Streams support callbacks for cleanup.
154
+
155
+ ---
156
+
157
+ ## Principle 8: IndiferentHash everywhere
158
+
159
+ Options and configuration are always IndiferentHash — symbol/string
160
+ indifferent. This eliminates symbol-vs-string bugs:
161
+
162
+ ```ruby
163
+ options = IndiferentHash.setup({})
164
+ options[:model] = "gpt-4"
165
+ options['model'] # → "gpt-4"
166
+ options[:model] # → "gpt-4"
167
+ ```
168
+
169
+ All option-processing utilities (`process_options`, `add_defaults`, `pull_keys`)
170
+ work on IndiferentHash. When writing code that accepts options, use these
171
+ utilities.
172
+
173
+ ---
174
+
175
+ ## Principle 9: Self-documenting through annotation propagation
176
+
177
+ When you extract items from annotated collections, they inherit the parent's
178
+ annotations:
179
+
180
+ ```ruby
181
+ list = Gene.setup(["BRCA1", "TP53"], :organism, "Hsa")
182
+ list.first.organism # → "Hsa" (inherited from parent list)
183
+ list.select { |g| g.length > 4 }.organism # → "Hsa" (preserved through select)
184
+ ```
185
+
186
+ This is the AnnotatedArray pattern: enumeration methods are overridden to
187
+ propagate annotations. The data carries its metadata with it.
188
+
189
+ ---
190
+
191
+ ## Principle 10: Compact DSLs over verbose configuration
192
+
193
+ Scout prefers compact, expressive DSLs:
194
+
195
+ ```ruby
196
+ # SOPT compact definition
197
+ SOPT.parse("-t--tool* tool to use
198
+ -d--database* database
199
+ -v--verbose")
200
+
201
+ # Path maps as a hash of templates
202
+ path_maps = {
203
+ current: "{PWD}/{TOPLEVEL}/{SUBPATH}",
204
+ home: "{HOME}/{TOPLEVEL}/{PKGDIR}/{SUBPATH}"
205
+ }
206
+ ```
207
+
208
+ Configuration is data (hashes, strings), not objects.
209
+
210
+ ---
211
+
212
+ ## Anti-patterns to avoid
213
+
214
+ ### 1. Creating wrapper classes for native types
215
+ ```ruby
216
+ # WRONG
217
+ class PathWrapper
218
+ def initialize(path); @path = path; end
219
+ def read; File.read(@path); end
220
+ end
221
+
222
+ # RIGHT — annotate
223
+ path = Path.setup("data/file.tsv")
224
+ path.read # works because Resource/path.rb adds read
225
+ ```
226
+
227
+ ### 2. Eager initialization
228
+ ```ruby
229
+ # WRONG
230
+ class Processor
231
+ def initialize
232
+ @cache = build_huge_cache() # expensive, always runs
233
+ end
234
+ end
235
+
236
+ # RIGHT — lazy
237
+ def cache
238
+ @cache ||= build_huge_cache()
239
+ end
240
+ ```
241
+
242
+ ### 3. Explicit delegation when method_missing already works
243
+ ```ruby
244
+ # WRONG — don't add these to a module that already uses method_missing
245
+ def data; @path.data; end
246
+ def results; @path.results; end
247
+
248
+ # RIGHT — method_missing handles it
249
+ ```
250
+
251
+ ### 4. Not using IndiferentHash for options
252
+ ```ruby
253
+ # WRONG — symbol/string fragility
254
+ def foo(options)
255
+ options[:key] || options['key'] # verbose, error-prone
256
+ end
257
+
258
+ # RIGHT
259
+ def foo(options)
260
+ options = IndiferentHash.setup(options) unless IndiferentHash === options
261
+ options[:key]
262
+ end
263
+ ```
264
+
265
+ ### 5. Breaking the annotate-don't-wrap principle for IO
266
+ ```ruby
267
+ # WRONG
268
+ class TrackedStream
269
+ def initialize(io); @io = io; end
270
+ end
271
+
272
+ # RIGHT — annotate the IO
273
+ ConcurrentStream.setup(io, :pids => [pid], :threads => [thread])
274
+ ```
275
+
276
+ ### 6. Forgetting to `join` streams
277
+ ```ruby
278
+ # WRONG — orphaned subprocess
279
+ stream = CMD.cmd("long_command", :pipe => true)
280
+ lines = stream.readlines # may leave process running if exception
281
+
282
+ # RIGHT
283
+ begin
284
+ stream = CMD.cmd("long_command", :pipe => true)
285
+ lines = stream.readlines
286
+ ensure
287
+ stream.join
288
+ end
289
+ ```
290
+
291
+ ### 7. Mutating global path configuration
292
+ ```ruby
293
+ # WRONG — affects all paths globally
294
+ Path.path_maps = { ... } # class-level mutation
295
+
296
+ # RIGHT — per-path or per-resource
297
+ path = Path.setup("data", path_maps: { ... }, map_order: [...])
298
+ ```
299
+
300
+ ---
301
+
302
+ ## Cross-cutting utilities (Misc module)
303
+
304
+ The `Misc` module is a grab-bag of utilities used across the codebase. It is
305
+ the "stdlib" of scout-essentials. Key categories:
306
+
307
+ ### Format utilities
308
+ - `Misc.format_paragraph(text, size, indent, offset)` — word-wrap text to fit
309
+ terminal width, preserving code blocks and lists.
310
+ - `Misc.format_definition_list(hash, sep)` — format a hash as `key: value`.
311
+ - `Misc.format_seconds(time)` — `HH:MM:SS` format.
312
+ - `Misc.format_seconds_short(time)` — human-readable short form.
313
+ - `Misc.colors_for(list)` — assign hex colors to unique elements.
314
+
315
+ ### Math utilities
316
+ - `Misc.log2(x)`, `Misc.log10(x)` — cached multiplier versions.
317
+ - `Misc.max`, `Misc.min`, `Misc.mean`, `Misc.stddev` — list statistics.
318
+ - `Misc.zip_fields(lists)` — transpose arrays.
319
+ - `Misc.bin_for_value`, `Misc.bins` — histogram binning.
320
+
321
+ ### Digest utilities
322
+ - `Misc.digest_str(obj)` — deterministic string representation for digesting.
323
+ - `Misc.digest(obj)` — MD5 of `digest_str`.
324
+ - `Misc.file_md5(path)` — MD5 of file content.
325
+ - `Misc.obj_md5(obj)` — MD5 of object representation.
326
+
327
+ ### Filesystem utilities
328
+ - `Misc.in_dir(dir) { ... }` — chdir + yield + restore.
329
+ - `Misc.path_relative_to(basedir, path)` — relative path computation.
330
+ - `Misc.add_libdir(dir)` — add lib directory to LOAD_PATH.
331
+ - `Misc.zip_zones`, `Misc.tar_files` — archive helpers.
332
+
333
+ ### System utilities
334
+ - `Misc.hostname` — cached hostname.
335
+ - `Misc.children(pid)` — child processes.
336
+ - `Misc.pid_alive?(pid)` — check if process is alive.
337
+
338
+ ### Process utilities
339
+ - `Misc.benchmark(repeats) { ... }` — benchmark and log.
340
+ - `Misc.insist(times, sleep) { ... }` — retry with exponential backoff.
341
+ - `Misc.pid_alive?(pid)` — check liveness.
342
+
343
+ ### Matching utilities
344
+ - `Misc.match_value(value, condition)` — fuzzy/comparison matching.
345
+ - `Misc._convert_match_condition(str)` — parse `>1`, `/regex/`, `!value`, etc.
346
+ - `Misc.intersect_sorted_arrays(a1, a2)` — efficient sorted intersection.
347
+
348
+ ---
349
+
350
+ ## Gotchas and warnings
351
+
352
+ 1. **Annotation `setup` on frozen objects** — `setup` dup's frozen objects,
353
+ which creates a new instance. If you hold a reference to the original
354
+ frozen object, it won't have the annotation.
355
+ 2. **`method_missing` can mask typos** — In Path, calling `path.typo_method`
356
+ will create a new path `"data/typo_method"` instead of raising. This is by
357
+ design but can cause subtle bugs.
358
+ - Mitigation: `path.method_missing` only fires if the method name doesn't
359
+ start with `to_` and no block is given.
360
+ 3. **IndiferentHash default value interaction** — if a hash has a `default`
361
+ or `default_proc`, the alternate-form lookup is bypassed. This can silently
362
+ return the default instead of trying the other key form.
363
+ 4. **ConcurrentStream callback ordering** — callbacks are chained in LIFO
364
+ order (most recently added runs first). If ordering matters, document it.
365
+ 5. **Persist cache path determinism** — the path is derived from the name +
366
+ options hash. If the options hash is not deterministic (e.g., contains a
367
+ Proc or a non-deterministic object), the cache path changes every run.
368
+ 6. **Resource produce race condition** — if two processes call `produce`
369
+ simultaneously on a non-existent resource, both will run the claim. The
370
+ lock prevents this, but only if both use the same lock path. Verify that
371
+ `Open.lock` is always called with the canonical lock path.
372
+ 7. **Log.severity is global** — changes affect all threads. Use
373
+ `Log.with_severity(level) { ... }` for scoped changes.
374
+ 8. **Misc.digest of a Path** — `Misc.digest` checks if the string is a valid
375
+ filename and if so, digests the file content. This is usually desired
376
+ (digesting the data, not the path string), but can be surprising.
377
+ 9. **Path.map_order as class variable** — `@@map_order` is shared across all
378
+ Path instances. Modifying it globally affects all paths. Per-instance
379
+ `@map_order` can be set during `Path.setup`.
380
+ 10. **Annotation propagation may not match expectations** — AnnotatedArray
381
+ overrides specific enumeration methods (`[]`, `first`, `last`, `select`,
382
+ `collect`, etc.). Methods not in the list (e.g., `filter_map`, `tally`)
383
+ will NOT propagate annotations.