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,774 @@
|
|
|
1
|
+
# Implementation inventory — chunk 3: Path / Persist / Resource / Annotation
|
|
2
|
+
|
|
3
|
+
Phase 1 forensic documentation audit of `/bulk/mvazque2/git/scout-essentials`.
|
|
4
|
+
Source under `lib/scout/` is authoritative; every claim below carries a `file:line`
|
|
5
|
+
anchor and, where behaviour is non-obvious, references a probe from
|
|
6
|
+
`research/behavior-probes.md` (P34–P44, scripts under `tmp/probe3*.rb`/`tmp/probe4*.rb`).
|
|
7
|
+
|
|
8
|
+
Scope (22 files):
|
|
9
|
+
`lib/scout/path.rb`, `path/digest.rb`, `path/find.rb`, `path/tmpfile.rb`, `path/util.rb`;
|
|
10
|
+
`lib/scout/persist.rb`, `persist/open.rb`, `persist/path.rb`, `persist/serialize.rb`;
|
|
11
|
+
`lib/scout/resource.rb`, `resource/open.rb`, `resource/path.rb`, `resource/produce.rb`,
|
|
12
|
+
`resource/produce/rake.rb`, `resource/scout.rb`, `resource/software.rb`, `resource/sync.rb`,
|
|
13
|
+
`resource/util.rb`; `lib/scout/annotation.rb`, `annotation/annotated_object.rb`,
|
|
14
|
+
`annotation/annotation_module.rb`, `annotation/array.rb`.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 1. Path subsystem
|
|
19
|
+
|
|
20
|
+
### 1.1 `lib/scout/path.rb` — the Path model itself
|
|
21
|
+
|
|
22
|
+
`Path` is a **module**, not a class, and it *is itself an annotated type*:
|
|
23
|
+
- `module Path; extend Annotation; annotation :pkgdir, :libdir, :path_maps, :map_order, :where, :original` (path.rb:6-8).
|
|
24
|
+
So `Path.setup(str)` produces a String extended with `Path` whose annotation set is
|
|
25
|
+
those six keys, and `annotation_types` of such a Path is `[Path]` (verified P36f).
|
|
26
|
+
|
|
27
|
+
Instance methods:
|
|
28
|
+
- `Path.default_pkgdir` → `'scout'` (path.rb:10-12); `Path.default_pkgdir=` writer (path.rb:14-16).
|
|
29
|
+
- `#pkgdir` → `@pkgdir ||= Path.default_pkgdir` (path.rb:18-20).
|
|
30
|
+
- `#libdir` → `@libdir || Path.caller_lib_dir` (path.rb:22-24) — falls back to a **caller-stack**
|
|
31
|
+
scan (`find.rb:24-49`) that walks up from the calling file looking for a dir containing
|
|
32
|
+
any of `lib`, `bin`, `README.md` (find.rb:24, default `relative_to = ['lib','bin','README.md']`).
|
|
33
|
+
- `#path_maps` → `@path_maps ||= Path.path_maps.dup` (path.rb:26-28) — per-instance copy;
|
|
34
|
+
mutations on an instance do not leak to the global table.
|
|
35
|
+
- `#join(subpath = nil, prevpath = nil)` (path.rb:30-40): `Symbol` args are stringified,
|
|
36
|
+
`prevpath` is prepended, empty receiver yields the subpath itself; result is re-annotated
|
|
37
|
+
with `self.annotate(new)` (path.rb:38). Aliases `[]` and `/` (path.rb:42-43).
|
|
38
|
+
- `#method_missing(name, prev = nil, *args, &block)` (path.rb:45-51): if a block is given
|
|
39
|
+
or the name starts with `to_` it defers to `super`; **otherwise** it builds a path
|
|
40
|
+
segment — i.e. `Scout.share.data` ≡ `Scout.share.join(:data)`. This is the
|
|
41
|
+
`Scout.etc["path_maps"]` / `Scout.share.software` mechanism.
|
|
42
|
+
- `Path` instances also carry `#where` (find.rb:211-213) and `#original`
|
|
43
|
+
(find.rb:215-217) readers, written by `#annotate_found_where` (find.rb:204-209) —
|
|
44
|
+
the map name a `find` succeeded under, and a self-annotated copy of the *unlocated*
|
|
45
|
+
original path.
|
|
46
|
+
|
|
47
|
+
Requires: `annotation`, `path/util`, `path/tmpfile`, `path/digest` (path.rb:1-4) and,
|
|
48
|
+
at the bottom, `path/find` (path.rb:53) — `find` is deliberately loaded after the
|
|
49
|
+
Path module body so `Path.extend Annotation` is in place first.
|
|
50
|
+
|
|
51
|
+
### 1.2 `lib/scout/path/find.rb` — resolution algorithm
|
|
52
|
+
|
|
53
|
+
Global (module-function) state, all memoised in class variables:
|
|
54
|
+
- `Path.path_maps` (find.rb:85-101): `IndiferentHash` of 13 default maps:
|
|
55
|
+
`:current "{PWD}/{TOPLEVEL}/{SUBPATH}"`, `:home "{HOME}/{TOPLEVEL}/{PKGDIR}/{SUBPATH}"`,
|
|
56
|
+
`:user "{HOME}/.{PKGDIR}/{TOPLEVEL}/{SUBPATH}"`, `:global`, `:usr`, `:local`, `:fast`,
|
|
57
|
+
`:cache`, `:bulk`, `:lib "{LIBDIR}/{TOPLEVEL}/{SUBPATH}"`,
|
|
58
|
+
`:scout_essentials_lib "<libdir>/{TOPLEVEL}/{SUBPATH}"` (computed from `caller_lib_dir(__FILE__)`),
|
|
59
|
+
`:tmp "/tmp/{PKGDIR}/{TOPLEVEL}/{SUBPATH}"`, and `:default => :user` — a *symbol alias*.
|
|
60
|
+
- `Path.basic_map_order` (find.rb:103-105): `%w(current workflow user home local global usr lib fast cache bulk)`.
|
|
61
|
+
Note `workflow` is listed but has no default map, so it is silently dropped.
|
|
62
|
+
- `Path.map_order` (find.rb:107-119): derived order = basic order with the literal `lib`
|
|
63
|
+
slot replaced by every `*_lib` map followed by `lib`, then `(basic & all) + (all - basic)`.
|
|
64
|
+
Observed default (P34/P39):
|
|
65
|
+
`[:current, :user, :home, :local, :global, :usr, :scout_essentials_lib, :lib, :fast, :cache, :bulk, :default, :tmp]`
|
|
66
|
+
— `:default` and `:tmp` come last because they are not in the basic list.
|
|
67
|
+
- Instance-level `#map_order` (find.rb:136-144) re-derives from the instance `path_maps`,
|
|
68
|
+
keeping the global order as a template. There is **no `Path.map_order=` setter** —
|
|
69
|
+
the only supported mutation entry points are `Path.add_path` / `prepend_path` / `append_path`
|
|
70
|
+
(find.rb:121-134, module-function) and the instance counterparts (find.rb:146-159).
|
|
71
|
+
`Path.map_order=` raises `NoMethodError` (verified P39).
|
|
72
|
+
`Path.add_path` resets `@@map_order = nil` (find.rb:123) so the order is recomputed and
|
|
73
|
+
the new map lands at the end; `prepend_path`/`append_path` unshift/push onto the
|
|
74
|
+
*already computed* order (find.rb:128, 133), so they do not reset it.
|
|
75
|
+
- `Path.load_path_maps(filename)` (find.rb:161-176): if the file exists, `YAML.load`s a
|
|
76
|
+
`{where: location}` mapping and `add_path`es each entry; failures are only logged
|
|
77
|
+
(`Log.error`, find.rb:171). Used at boot by `resource/scout.rb:9`.
|
|
78
|
+
|
|
79
|
+
Substitution engine — `Path.follow(path, map, map_name = nil)` (find.rb:51-83):
|
|
80
|
+
- a map without `{` gets `/{PATH}` appended (find.rb:52);
|
|
81
|
+
- `{PKGDIR}` uses the path's (or module-level default) pkgdir, unwrapping nested
|
|
82
|
+
pkgdir-reponding objects (find.rb:53-57);
|
|
83
|
+
- substitution order: `{PKGDIR}`, `{HOME}`, `{RESOURCE}` (also `path.pkgdir.to_s`),
|
|
84
|
+
`{PWD}` (via `FileUtils.pwd`), `{TOPLEVEL}` (`_toplevel`), `{SUBPATH}` (`_subpath`),
|
|
85
|
+
`{BASENAME}`, `{PATH}`, `{LIBDIR}`, `{MAPNAME}`, `{REMOVE}/`, `{REMOVE}` (find.rb:58-69);
|
|
86
|
+
- trailing `/` is dropped when the path has no subpath (find.rb:71);
|
|
87
|
+
- then a loop rewrites nested `{KEY/ORIG/REPLACE}` forms by recursive `follow`
|
|
88
|
+
(find.rb:73-80).
|
|
89
|
+
|
|
90
|
+
Decomposition helpers (find.rb:178-188): `#_parts` (`split("/")` memoised),
|
|
91
|
+
`#_subpath` (all but first part, or `nil`), `#_toplevel` (first part). For a single
|
|
92
|
+
segment path `_toplevel == self` and `_subpath == nil` (see test `test_find.rb`).
|
|
93
|
+
|
|
94
|
+
`Path.located?(path)` (find.rb:193-198): true iff the string starts with `/`, `~/`, or `./`
|
|
95
|
+
(byte comparisons `SLASH = "/"[0]`, `HOME = "~"[0]`, `DOT = "."[0]`, find.rb:190-192).
|
|
96
|
+
Instance `#located?` delegates (find.rb:200-202).
|
|
97
|
+
|
|
98
|
+
`Path.exists_file_or_alternatives(file)` (find.rb:237-245): returns the file itself if
|
|
99
|
+
`Open.exist?` or `Open.directory?`; otherwise tries `file + ".gz" / ".bgz" / ".zip"`
|
|
100
|
+
in that fixed order; otherwise `nil`.
|
|
101
|
+
|
|
102
|
+
**`Path#find(where = nil)`** (find.rb:247-274) — the core resolution:
|
|
103
|
+
1. If `located?`: absolute path exists → return `self.annotate(File.expand_path(self))`;
|
|
104
|
+
else check alternatives (`Path.exists_file_or_alternatives`) and annotate+return the
|
|
105
|
+
alternative if present; **else return `self` unchanged** (find.rb:248-259). It never
|
|
106
|
+
returns `nil` for located paths (verified P34: missing located path returns itself).
|
|
107
|
+
2. `where == 'all' || :all` → `find_all` (find.rb:261).
|
|
108
|
+
3. `where` given (Symbol/String) → `follow(where)` (find.rb:263). If the name is a String
|
|
109
|
+
not in `path_maps`, `follow` synthesises `File.join(map_name, '{TOPLEVEL}/{SUBPATH}')`
|
|
110
|
+
(find.rb:222-224); an unknown Symbol still raises `"Map not found"` (find.rb:225).
|
|
111
|
+
4. Otherwise iterate `map_order`, skipping names not present in `path_maps`
|
|
112
|
+
(find.rb:265-271): `follow(map_name, false)` (no annotation), then
|
|
113
|
+
`Path.exists_file_or_alternatives`; first hit is returned via
|
|
114
|
+
`annotate_found_where(found, map_name)` which sets `@where` and `@original`
|
|
115
|
+
(find.rb:269-270). The alternative check considers gz/bgz/zip suffixes and, when only
|
|
116
|
+
a compressed variant is on disk, find returns the suffixed path annotated with the
|
|
117
|
+
winning map name (verified P44).
|
|
118
|
+
5. Nothing found → **fall through to `follow(:default)`** (find.rb:273), i.e. by default
|
|
119
|
+
the `:user` map location (`{HOME}/.{PKGDIR}/{TOPLEVEL}/{SUBPATH}`), *not* nil
|
|
120
|
+
(verified P34: `/tmp/nonexistent…` returned self; unlocated `share/data/some_file`
|
|
121
|
+
returned `~/.scout/share/…`).
|
|
122
|
+
|
|
123
|
+
Related predicates:
|
|
124
|
+
- `#exist?` / `#exists?` (find.rb:276-283): `File.exist?(self.find) || File.directory?(…)`.
|
|
125
|
+
- `#find_all(caller_lib = nil, search_paths = nil)` (find.rb:284-288): maps `map_order`
|
|
126
|
+
through `find`, selects existing, `uniq`. (The `caller_lib`/`search_paths` params are
|
|
127
|
+
accepted but unused.)
|
|
128
|
+
- `#find_with_extension(extension, *args, produce: true)` (find.rb:290-303): plain find
|
|
129
|
+
first; if that exists and is not a directory it wins; otherwise try
|
|
130
|
+
`set_extension(ext).find` for each extension (Array or single) and return the first
|
|
131
|
+
that exists; **fall back to the un-extended `found`** (find.rb:302).
|
|
132
|
+
|
|
133
|
+
### 1.3 `lib/scout/path/util.rb` — filesystem helpers on Path
|
|
134
|
+
|
|
135
|
+
- `Path.is_filename?(string, need_to_exists = true)` (util.rb:8-16): `Path` passes; a
|
|
136
|
+
String passes if it has no newline, is < 265 chars (or no component > 265), and — with
|
|
137
|
+
`need_to_exists` — `File.exist?`.
|
|
138
|
+
- `Path.can_read?` / `can_write?` (util.rb:18-33): remote URLs are always "readable" but
|
|
139
|
+
never "writable"; otherwise `is_filename?` + not-a-directory checks, `find` applied for
|
|
140
|
+
Path instances, and for `can_write?` writability of the file or of its parent dir.
|
|
141
|
+
- `Path.sanitize_filename(filename, length = 254)` (util.rb:35-50): overlong names are
|
|
142
|
+
truncated and suffixed `--<len>--<digest5>` plus a preserved 2–9 char extension.
|
|
143
|
+
- `#directory?` → `nil` unless `exist?`, else `File.directory?(self.find)` (util.rb:52-55).
|
|
144
|
+
- `#realpath`, `#relative_to(dir)` (util.rb:57-63) — both operate on `find`.
|
|
145
|
+
- `#sub`, `#dirname`, `#basename` (util.rb:65-75) re-annotate the result.
|
|
146
|
+
- `#glob(pattern = "*")` (util.rb:77-97): a receiver containing `*` is globbed directly
|
|
147
|
+
when located, else via `glob_all`; otherwise the receiver must `exist?`, results are
|
|
148
|
+
annotated and, if the found location has an `original`, each result's `original` is
|
|
149
|
+
rewritten relative to it (util.rb:91-93).
|
|
150
|
+
- `#glob_names(...)` (util.rb:99-101) = `glob(...).collect(&:basename)`.
|
|
151
|
+
- `#glob_all(pattern = nil, caller_lib = nil, search_paths = nil)` (util.rb:103-122):
|
|
152
|
+
iterates every entry of `self.path_maps` (or `Path.path_maps`), calls `find(where)` per
|
|
153
|
+
entry, keeps only located results, globs each, annotates, and sets `original`/`where`
|
|
154
|
+
when a pattern was given.
|
|
155
|
+
- Extension helpers: `#get_extension(multiple = false)` (util.rb:124-133; when `multiple`
|
|
156
|
+
collects up to 4 extra dotted segments), `#set_extension(ext)` (util.rb:135-137, appends
|
|
157
|
+
`.ext`), `#unset_extension` (util.rb:139-146), `#remove_extension(ext = nil)`
|
|
158
|
+
(util.rb:148-154), `#replace_extension(new_ext, multiple = false)` (util.rb:156-166).
|
|
159
|
+
- Freshness: `#newer_files(*files)` (util.rb:168-172) and `#outdated?(...)` (util.rb:174-176)
|
|
160
|
+
select files newer than self; `Path.newer?(path, file, by_link = false)`
|
|
161
|
+
(util.rb:179-194) returns a *numeric age difference* (negative) when the file is newer
|
|
162
|
+
than the path, `false` otherwise, `true` when `file` does not exist — it is truthy/falsy
|
|
163
|
+
tri-state, not a boolean. Uses `Open.mtime` (so `<file>.info` Step blobs participate,
|
|
164
|
+
see chunk-2 findings) unless `by_link` (then `File.lstat.mtime`).
|
|
165
|
+
- `#final_pkgdir` (util.rb:196-200): unwraps pkgdir chains (`pkgdir.pkgdir while …`).
|
|
166
|
+
|
|
167
|
+
### 1.4 `lib/scout/path/digest.rb` — `#digest_str`
|
|
168
|
+
|
|
169
|
+
`Path#digest_str` (digest.rb:3-22):
|
|
170
|
+
- directory → `glob("*")`, reject directories, `Annotation.purge` the annotated results,
|
|
171
|
+
and emit `"Directory MD5: <n> <digest>"` when > 10 files else `"Directory MD5: <digest>"`;
|
|
172
|
+
- existing located file → `"File MD5: <Misc.digest_file(self)>"`;
|
|
173
|
+
- otherwise `'\'' + self << '\''` (note: literal quote wrapping, digest.rb:20 — this is a
|
|
174
|
+
quoting quirk, not an error).
|
|
175
|
+
|
|
176
|
+
### 1.5 `lib/scout/path/tmpfile.rb`
|
|
177
|
+
|
|
178
|
+
`TmpFile.with_path(*args, &block)` (tmpfile.rb:2-7) wraps `TmpFile.with_file` and
|
|
179
|
+
`Path.setup`s the generated temp name before yielding. This is the only content; the
|
|
180
|
+
tmp-root/digest conventions live in `lib/scout/tmpfile.rb` (chunk-1 inventory).
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 2. Persist subsystem
|
|
185
|
+
|
|
186
|
+
### 2.1 `lib/scout/persist.rb` — cache/lock roots, `persistence_path`, `persist`
|
|
187
|
+
|
|
188
|
+
Class-level configuration:
|
|
189
|
+
- `Persist.cache_dir` defaults to `Path.setup("var/cache/persistence")` (persist.rb:12-14)
|
|
190
|
+
— a **relative** Path; `cache_dir=` accepts a String or Path (persist.rb:8-10).
|
|
191
|
+
- `Persist.lock_dir` defaults to `Path.setup("tmp/persist_locks").find` (persist.rb:16-19),
|
|
192
|
+
which resolves through the default path maps to `$HOME/.scout/tmp/persist_locks`
|
|
193
|
+
(observed P41: `/home/mvazque2/.scout/tmp/persist_locks`).
|
|
194
|
+
|
|
195
|
+
**`Persist.persistence_path(name, options = {})`** (persist.rb:22-28):
|
|
196
|
+
- `options` is an options hash; the defaults add `:dir => Persist.cache_dir`;
|
|
197
|
+
"other" options are pulled out and appended by `TmpFile.tmp_for_file` as
|
|
198
|
+
`:<digest>` (observed P41: `…/foo:41a66144f5d092dbc008b979700699d4`).
|
|
199
|
+
- `Persist.persistence_path(name, :marshal)` is **not valid** — the symbol is treated as
|
|
200
|
+
the options hash and fails with `TypeError: can't define singleton` (P41). The type is
|
|
201
|
+
*never* passed to `persistence_path`; `Persist.persist` takes it as a separate
|
|
202
|
+
positional parameter.
|
|
203
|
+
- A `:key` option yields `name + "[key]"` via TmpFile's clean-options naming
|
|
204
|
+
(observed P41: `…/foo[K]`), following the same `MAX_FILE_LENGTH`/digest truncation rules
|
|
205
|
+
as all TmpFile names (`tmpfile.rb`, chunk-1).
|
|
206
|
+
|
|
207
|
+
`Persist.persist(name, type = :serializer, options = {}, &block)` (persist.rb:32-144):
|
|
208
|
+
1. pulls `:persist`-prefixed sub-options; `:persist => false` short-circuits to `yield`
|
|
209
|
+
(persist.rb:33-34).
|
|
210
|
+
2. `file = persist_options[:path] || options[:path] || persistence_path(name, options)`
|
|
211
|
+
(persist.rb:36); `:data`, `:check`, `:no_load`, `:update` read from either level
|
|
212
|
+
(persist.rb:37-41). `update` may be a Time, a Numeric (seconds of staleness) or truthy
|
|
213
|
+
(persist.rb:45-49); `Path`-valued `update` is converted with `Open.mtime` (persist.rb:42).
|
|
214
|
+
If `file` is a Path and `check` is set, `update` is forced when the file is outdated
|
|
215
|
+
(persist.rb:44).
|
|
216
|
+
3. `type == :memory` bypasses all locking and files: repo is `options[:memory] ||
|
|
217
|
+
options[:repo] || MEMORY_CACHE`, keyed by `file`; `update` recomputes, otherwise the
|
|
218
|
+
cached value is reused (persist.rb:51-59). `Persist.memory(name, options, &block)`
|
|
219
|
+
(persist.rb:146-149) is a thin wrapper that derives a `:path`/`:persist_path` from
|
|
220
|
+
`[name, options[:key]] * ":"` when a `:key` is given.
|
|
221
|
+
4. Otherwise a lockfile is built as `persistence_path(file + '.persist',
|
|
222
|
+
:dir => Persist.lock_dir)` (persist.rb:61) and the whole read-or-compute critical
|
|
223
|
+
section runs inside `Open.lock` (persist.rb:63), i.e. the chunk-2 `Lockfile` payload
|
|
224
|
+
mechanism with `LockInterrupted` semantics.
|
|
225
|
+
5. Cache hit (`Open.exist?(file) && ! update`): with `:no_load => true` return the file
|
|
226
|
+
path, else `Persist.load(file, type)` (persist.rb:64-69).
|
|
227
|
+
6. Miss: `Open.rm(file.find)` if updating (persist.rb:72); `file = file.find` when it is a
|
|
228
|
+
Path (persist.rb:74). A block with `arity == 1` is called with either the caller's
|
|
229
|
+
`:data` or the file name (persist.rb:75-81); arity 0 blocks are plain `yield`
|
|
230
|
+
(persist.rb:83).
|
|
231
|
+
7. `res.nil?` handling (persist.rb:86-99): with `no_load` return the file; else with a nil
|
|
232
|
+
`type` return nil; else `Persist.load(file, type)`.
|
|
233
|
+
8. Stream results (`IO`/`StringIO`): tee into `tee_copies + 1` streams (default 1 copy,
|
|
234
|
+
persist.rb:102), mark `main.lock = lock`, spawn a saver thread running
|
|
235
|
+
`Open.sensible_write(file, main)` (persist.rb:103-110), wrap every copy as a
|
|
236
|
+
`ConcurrentStream` with `:threads => t, :filename => file, :autojoin => true, :next => …`
|
|
237
|
+
(persist.rb:111-114) and return the first copy, raising `KeepLocked.new(res)` so the
|
|
238
|
+
outer `Open.lock` keeps the lockfile until the stream joins (persist.rb:116; the
|
|
239
|
+
`KeepLocked < DontPersist` taxonomy is established in chunk 1/2).
|
|
240
|
+
9. Non-stream results are written via `Persist.save(res, file, type)` and `res` is
|
|
241
|
+
replaced by the save result unless it is nil (persist.rb:118-119) — for file-backed
|
|
242
|
+
types `Persist.save` returns nil, so the *block's* value is returned, not the file path.
|
|
243
|
+
10. Error path (persist.rb:121-135): on any `Exception`, the partially written file is
|
|
244
|
+
removed under `Thread.handle_interrupt(Exception => :never)` unless the exception is a
|
|
245
|
+
`DontPersist` (persist.rb:130), and the exception is re-raised unless
|
|
246
|
+
`options[:canfail]` (persist.rb:133) — with `:canfail` execution falls through to
|
|
247
|
+
persist.rb:137-141 returning `file` when `no_load` is exactly `true`, else `res`
|
|
248
|
+
(which may be nil).
|
|
249
|
+
11. Final return for a successful write: `file` if `no_load == true` else `res`
|
|
250
|
+
(persist.rb:137-141).
|
|
251
|
+
|
|
252
|
+
### 2.2 `lib/scout/persist/serialize.rb` — types, drivers, save/load
|
|
253
|
+
|
|
254
|
+
Constants: `TRUE_STRINGS` (Set of 13 truthy spellings, serialize.rb:6) and
|
|
255
|
+
`SERIALIZER = :json` (serialize.rb:7) — **the default serializer type is JSON, not marshal**.
|
|
256
|
+
|
|
257
|
+
Pluggable drivers: `save_drivers` / `load_drivers` hashes with accessors
|
|
258
|
+
(serialize.rb:9-17); `save` consults `save_drivers[type]` first, calling it with
|
|
259
|
+
`(content)` when its arity is 1 (result written with `Open.sensible_write`) or
|
|
260
|
+
`(file, content)` otherwise (serialize.rb:103-109); `load` consults `load_drivers[type]`
|
|
261
|
+
(serialize.rb:133-135).
|
|
262
|
+
|
|
263
|
+
`Persist.serialize(content, type)` (serialize.rb:19-47):
|
|
264
|
+
- `nil, :string, :text, :integer, :float, :boolean, :file, :path, :select, :folder,
|
|
265
|
+
:binary` → `content.read` for IO/StringIO else `content.to_s` (serialize.rb:23-28);
|
|
266
|
+
- `:array` → `content * "\n"` (serialize.rb:29-30);
|
|
267
|
+
- `:yaml` / `:json` / `:marshal` → `to_yaml` / `to_json` / `Marshal.dump` (serialize.rb:31-36);
|
|
268
|
+
- `:annotation, :annotations` → `Annotation.tsv(content, :all).to_s` (serialize.rb:37-38) —
|
|
269
|
+
**requires an external `Annotation.tsv` that this gem does not define** (there is no
|
|
270
|
+
`Annotation#tsv` anywhere under `lib/`; `grep` for `def tsv` in `annotation*.rb` is
|
|
271
|
+
empty) and therefore raises `NoMethodError: undefined method 'tsv' for module Annotation`
|
|
272
|
+
at runtime (verified P42/P38);
|
|
273
|
+
- `:serializer` is rewritten to `SERIALIZER` (:json) first (serialize.rb:21);
|
|
274
|
+
- any `*_array` suffix is expanded recursively and joined with newlines (serialize.rb:40-43);
|
|
275
|
+
- anything else raises `"Persist does not know …"` (serialize.rb:44).
|
|
276
|
+
|
|
277
|
+
`Persist.deserialize(serialized, type)` (serialize.rb:49-85):
|
|
278
|
+
- `nil, :text, :stream` → as-is (serialize.rb:54-55); `:string, :file, :select, :folder` →
|
|
279
|
+
`strip` (serialize.rb:56-57); `:path` → `Path.setup(serialized.strip)` (serialize.rb:58-59);
|
|
280
|
+
- `:integer`/`:float` → `to_i`/`to_f`; `:boolean` → membership in `TRUE_STRINGS`
|
|
281
|
+
(serialize.rb:60-65);
|
|
282
|
+
- `:array` → `split("\n")` (serialize.rb:66-67);
|
|
283
|
+
- `:yaml` → **`YAML.parse`** (serialize.rb:68-69), i.e. a Psych AST node, *not* a loaded
|
|
284
|
+
Ruby object — a documented-in-code asymmetry with `Persist.load(file, :yaml)` which uses
|
|
285
|
+
`Open.yaml` (serialize.rb:141). Verified P35: deserializing a YAML string returns a
|
|
286
|
+
`Psych::Nodes::Document`.
|
|
287
|
+
- `:json` → `JSON.parse`; `:marshal` → `Marshal.load` (serialize.rb:70-73);
|
|
288
|
+
- `:annotation, :annotations` → `Annotation.load_tsv(TSV.open(serialized))`
|
|
289
|
+
(serialize.rb:74-75) — depends on **both** an external `TSV` class and an external
|
|
290
|
+
`Annotation.load_tsv`; neither is defined in this gem (`grep TSV lib/` only matches the
|
|
291
|
+
two call sites plus Log/progress references). So the `:annotation` persistence type is
|
|
292
|
+
*declared but not functional* standalone (verified P38: `Persist.persist(…, :annotation, …)`
|
|
293
|
+
raises `NoMethodError`).
|
|
294
|
+
- `*_array` recursion as in serialize (serialize.rb:77-80); unknown raises
|
|
295
|
+
(serialize.rb:82).
|
|
296
|
+
|
|
297
|
+
`Persist.save(content, file, type = :serializer)` (serialize.rb:88-122):
|
|
298
|
+
- normalises `nil` type to `:serializer`; `:memory` swaps in the shared `MEMORY` hash;
|
|
299
|
+
returns immediately on `nil` content (serialize.rb:89-95) — note `type == :memory` is
|
|
300
|
+
checked twice (serialize.rb:92 and :94, dead line);
|
|
301
|
+
- `Hash === type` writes `type[file] = content` (in-memory repo) and returns (serialize.rb:97-100);
|
|
302
|
+
- driver hook (serialize.rb:103-109);
|
|
303
|
+
- `:binary` forces `ASCII-8BIT` and writes with `f.puts` in `wb` mode, returning
|
|
304
|
+
`content` (serialize.rb:111-116);
|
|
305
|
+
- everything else: `serialize(content, type)` + `Open.sensible_write(file, serialized,
|
|
306
|
+
:force => true)` and **returns nil** (serialize.rb:117-121) — which is why
|
|
307
|
+
`Persist.persist` keeps the block value for file-backed types (persist.rb:118-119).
|
|
308
|
+
|
|
309
|
+
`Persist.load(file, type = :serializer)` (serialize.rb:124-169):
|
|
310
|
+
- `file = file.find if Path === file` (serialize.rb:125); returns `nil` unless the type is
|
|
311
|
+
a Hash (memory repo) or the file exists (serialize.rb:130);
|
|
312
|
+
- special-cased loaders: `:binary` (`Open.read … :mode => 'rb'`), `:yaml` (`Open.yaml`, i.e.
|
|
313
|
+
`YAML.unsafe_load`), `:json` (`Open.json`), `:marshal`/`:serializer` (`Open.marshal`),
|
|
314
|
+
`:stream` (`Open.open(file)`), `:path` (return the Path itself), `:file` (read the file,
|
|
315
|
+
rewrite a leading `./` to the file's dirname, and return that value only if it
|
|
316
|
+
`Path.is_filename?`, else return `file`), `:file_array` (same per line)
|
|
317
|
+
(serialize.rb:137-162);
|
|
318
|
+
- `Hash` type → repo lookup (serialize.rb:163-164);
|
|
319
|
+
- otherwise generic `Open.read` + `deserialize` (serialize.rb:165-168). Note `:file_array`
|
|
320
|
+
exists as a *load-only* special case; its serialization goes through the generic
|
|
321
|
+
`*_array` machinery (`:file` + "\n").
|
|
322
|
+
|
|
323
|
+
### 2.3 `lib/scout/persist/open.rb` — typed readers
|
|
324
|
+
|
|
325
|
+
`Open.json(file)` (open.rb:6-9) and `Open.yaml(file)` (open.rb:11-14) both call
|
|
326
|
+
`file.find_with_extension(:json|:yaml)` when given a Path — so they transparently fall
|
|
327
|
+
back to `.json.gz`-style alternatives per `Path#find_with_extension`.
|
|
328
|
+
`Open.yaml` uses **`YAML.unsafe_load`** (open.rb:13). `Open.marshal(file)` (open.rb:16-18)
|
|
329
|
+
has no extension fallback.
|
|
330
|
+
|
|
331
|
+
### 2.4 `lib/scout/persist/path.rb` — instance sugar
|
|
332
|
+
|
|
333
|
+
Reopens `Path` to add `#yaml`, `#json`, `#marshal` delegating to the corresponding
|
|
334
|
+
`Open.*` readers (path.rb:2-14). Loaded by `persist.rb:3` only, so these methods exist
|
|
335
|
+
once `scout/persist` has been required.
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
## 3. Resource subsystem
|
|
340
|
+
|
|
341
|
+
### 3.1 `lib/scout/resource.rb` — the Resource module itself
|
|
342
|
+
|
|
343
|
+
`module Resource; extend Annotation; annotation :pkgdir, :libdir, :subdir, :resources,
|
|
344
|
+
:rake_dirs, :path_maps, :map_order, :lock_dir` (resource.rb:19-21). Like `Path`, `Resource`
|
|
345
|
+
is an *annotated module*: a resource is any module that `extend`s `Resource` and thereby
|
|
346
|
+
gains `claim`, `produce`, `identify`, … plus per-instance annotation ivars.
|
|
347
|
+
|
|
348
|
+
The file `require_relative`s its parts **twice** (resource.rb:3-8 and :10-17) — harmless
|
|
349
|
+
(`require_relative` is idempotent) but noteworthy.
|
|
350
|
+
|
|
351
|
+
Class-level: `Resource.default_resource` accessor (resource.rb:23-29). `Resource.default_lock_dir`
|
|
352
|
+
= `Path.setup('tmp/produce_locks').find` (resource.rb:31-33) → resolves to
|
|
353
|
+
`$HOME/.scout/tmp/produce_locks` (observed P43).
|
|
354
|
+
|
|
355
|
+
Instance (i.e. per-resource) helpers:
|
|
356
|
+
- `#path_maps` / `#map_order` duplicate the Path globals (resource.rb:35-41) so a resource
|
|
357
|
+
can be scoped independently;
|
|
358
|
+
- `#prepend_path(name, map)` / `#append_path(name, map)` (resource.rb:43-51) mutate that copy;
|
|
359
|
+
- `#subdir` defaults to `""` (resource.rb:53-55); `#lock_dir` defaults to
|
|
360
|
+
`Resource.default_lock_dir` (resource.rb:57-59); `#pkgdir` defaults to
|
|
361
|
+
`Path.default_pkgdir` (resource.rb:61-63);
|
|
362
|
+
- `#root` → `Path.setup(subdir.dup, self, self.libdir, @path_maps, @map_order)`
|
|
363
|
+
(resource.rb:65-67) — the resource acts as the *pkgdir annotation* of its root Path,
|
|
364
|
+
which is how `Path#produce` finds its way back to the claiming resource
|
|
365
|
+
(`resource/path.rb:6`);
|
|
366
|
+
- `#method_missing(name, prev = nil, *args)` (resource.rb:69-75) forwards to `root`,
|
|
367
|
+
reproducing the Path segment-building ergonomics at resource level (`Scout.etc`,
|
|
368
|
+
`Scout.share`, `Scout.lib`, …).
|
|
369
|
+
|
|
370
|
+
### 3.2 `lib/scout/resource/scout.rb` — bootstrapping `Scout` as the default resource
|
|
371
|
+
|
|
372
|
+
- `module Scout; extend Resource; self.pkgdir = 'scout'` (scout.rb:1-5) — the top-level
|
|
373
|
+
`Scout` namespace *is* a Resource with pkgdir `'scout'` (and `Path.default_pkgdir`
|
|
374
|
+
is also `'scout'`, path.rb:11).
|
|
375
|
+
- `Resource.default_resource = Scout` (scout.rb:7).
|
|
376
|
+
- `Path.load_path_maps(Scout.etc["path_maps"])` (scout.rb:9) — at require time the
|
|
377
|
+
`etc/path_maps` YAML (if it exists) augments the built-in map table; failures are logged
|
|
378
|
+
and swallowed (find.rb:170-172).
|
|
379
|
+
|
|
380
|
+
### 3.3 `lib/scout/resource/path.rb` — Path#produce and friends
|
|
381
|
+
|
|
382
|
+
`Path#produce(force = false)` (path.rb:2-21):
|
|
383
|
+
- re-raises a previously stored `@produced` if it is an Exception (path.rb:3) — failures
|
|
384
|
+
are memoised and re-raised on every subsequent call;
|
|
385
|
+
- returns `self` when not forcing and either the located file exists or a previous
|
|
386
|
+
produce succeeded (path.rb:4);
|
|
387
|
+
- dispatches to `self.pkgdir.produce(self, force)` when the pkgdir annotation is a
|
|
388
|
+
Resource (path.rb:6-7), otherwise returns `false` (path.rb:8-10) — a Path with no
|
|
389
|
+
Resource pkgdir **silently returns false**;
|
|
390
|
+
- `ResourceNotFound` is caught and turned into `@produced = false` (path.rb:11-12), so a
|
|
391
|
+
missing claim makes `produce` return `false` rather than raise;
|
|
392
|
+
- any other exception is logged with `Log.warn` and re-raised (path.rb:13-17);
|
|
393
|
+
- `ensure` sets `@produced = true` if still nil (path.rb:18-20) — so a *successful*
|
|
394
|
+
no-op (nothing to do) is memoised as `true`, a `ResourceNotFound` leaves `false`.
|
|
395
|
+
Consequence (verified P37c): requesting `.gz` when only the un-compressed name is
|
|
396
|
+
claimed makes `produce` return `false` the first time (extension fall-through found
|
|
397
|
+
the `.gz` claim to be absent) yet `find` still returns the `.gz` name that was created
|
|
398
|
+
by the claim registered for it; conversely requesting `h` when `h.gz` is claimed
|
|
399
|
+
produces the `.gz` file and `find` returns the `.gz` path.
|
|
400
|
+
|
|
401
|
+
`Path#produce_with_extension(extension, *args)` (path.rb:23-34): try `produce`, on
|
|
402
|
+
exception try `set_extension(extension).produce`, re-raise the *first* exception if the
|
|
403
|
+
second also fails.
|
|
404
|
+
|
|
405
|
+
`Path#produce_and_find(extension = nil, *args)` (path.rb:36-47): `find_with_extension` /
|
|
406
|
+
`find`; if the result exists return it, otherwise produce (with extension) and raise
|
|
407
|
+
`"Not found: #{self}"` unless something truthy came back.
|
|
408
|
+
|
|
409
|
+
`Path#relocate` (path.rb:49-52): `self` if it already exists, else `Resource.relocate(self)`.
|
|
410
|
+
`Path#identify` (path.rb:54-56) delegates to `Resource.identify`.
|
|
411
|
+
|
|
412
|
+
Convenience IO: `#open(*args, &block)` and `#read` call `produce` first then `Open.open`/
|
|
413
|
+
`Open.read` (path.rb:58-66); `#write(*args, &block)` writes to `self.find` **without**
|
|
414
|
+
producing (path.rb:68-70); `#list` = `produce_and_find('list')` + `Open.list` (path.rb:72-75).
|
|
415
|
+
|
|
416
|
+
`Path#exists?(produce: true)` (path.rb:77-85) — redefined here on top of the base
|
|
417
|
+
`Path#exist?` (find.rb:276) to optionally *produce* the path before concluding.
|
|
418
|
+
|
|
419
|
+
`Path#find_with_extension(extension, *args, produce: true)` (path.rb:87-100) — redefined
|
|
420
|
+
here so each candidate is checked with `exists?(produce: produce)`; the base version in
|
|
421
|
+
`path/find.rb:290` is shadowed once `resource/path` is loaded.
|
|
422
|
+
|
|
423
|
+
### 3.4 `lib/scout/resource/produce.rb` — claims and the produce engine
|
|
424
|
+
|
|
425
|
+
`Resource#claim(path, type, content = nil, &block)` (produce.rb:6-14):
|
|
426
|
+
- `type == :rake` registers in `@rake_dirs[path] = content || block`;
|
|
427
|
+
- everything else registers `@resources[path] = [type, content || block]`.
|
|
428
|
+
There is **no validation of `type`** at claim time; the registry is keyed by the raw
|
|
429
|
+
path string.
|
|
430
|
+
|
|
431
|
+
Lookup helpers: `#rake_for(path)` (produce.rb:16-23) selects rake dirs that are prefixes
|
|
432
|
+
of `path` (`Misc.path_relative_to`) sorted by length, taking the longest;
|
|
433
|
+
`#has_rake?(path)` (produce.rb:25-27).
|
|
434
|
+
|
|
435
|
+
`#run_rake(path, rakefile, rake_dir)` (produce.rb:29-51): computes the task as the path
|
|
436
|
+
relative to the rake dir; `produce`s/`find`s the rakefile if it responds; resolves
|
|
437
|
+
`rake_dir.find(:user)`; sets `Thread.current["resource"] = self`; calls `ScoutRake.run`
|
|
438
|
+
(with the block when the rakefile is a Proc); on `ScoutRake::TaskNotFound` it walks *up*
|
|
439
|
+
one directory level (`task = File.join(File.basename(rake_dir), task)`,
|
|
440
|
+
`rake_dir = File.dirname(rake_dir)`) and retries, re-raising at the filesystem root.
|
|
441
|
+
|
|
442
|
+
**`Resource#produce(path, force = false)`** (produce.rb:53-158) — the engine:
|
|
443
|
+
1. Claim selection (produce.rb:54-77): exact match in `@resources`; else match on
|
|
444
|
+
`path.original` (the unlocated original when a *found* path was passed in); else a
|
|
445
|
+
rake claim; else — for paths not ending in `.gz`/`.bgz` — **automatic extension
|
|
446
|
+
fall-through**: retry with `path + '.gz'` and then `path + '.bgz'`, and only then raise
|
|
447
|
+
`ResourceNotFound "Resource is missing and does not seem to be claimed: …"`
|
|
448
|
+
(produce.rb:64-76). Paths that already end in `.gz`/`.bgz` raise `ResourceNotFound`
|
|
449
|
+
directly (produce.rb:74-76).
|
|
450
|
+
2. Target resolution: `path.find(:default)` when forcing, `path.find` otherwise
|
|
451
|
+
(produce.rb:79-83).
|
|
452
|
+
3. Production runs only when a type exists and the target is missing or forcing
|
|
453
|
+
(produce.rb:85). Locking: `lock_filename = TmpFile.tmp_for_file(final_path,
|
|
454
|
+
:dir => lock_dir)` then `Open.lock lock_filename { … }` (produce.rb:87-89) — a
|
|
455
|
+
per-resource lock dir (default `$HOME/.scout/tmp/produce_locks`) and a lock *name
|
|
456
|
+
derived from the final path*, following TmpFile digest naming. `force` removes the
|
|
457
|
+
existing target before the inner check (produce.rb:90).
|
|
458
|
+
4. Dispatch on `type` (produce.rb:94-144), each arm wrapped in a rescue that removes the
|
|
459
|
+
partial target and re-raises (produce.rb:145-148):
|
|
460
|
+
- `:string` → `Open.sensible_write(final_path, content)` (produce.rb:96-97);
|
|
461
|
+
- `:csv` → **raises `"TSV/CSV Not implemented yet"` immediately** (produce.rb:98-102,
|
|
462
|
+
with the commented-out `rbbt/tsv/csv` implementation left in place);
|
|
463
|
+
- `:url` → `Open.sensible_write(final_path, Open.open(content, options))` with
|
|
464
|
+
`:noz => true` when the target is compressed (produce.rb:103-106) (remote fetch via
|
|
465
|
+
`Open.wget`/curl, see chunk 2);
|
|
466
|
+
- `:proc` → call with arity 0 or 1 (the target path) (produce.rb:107-113); the result is
|
|
467
|
+
written when it is `String, IO, StringIO` (direct), `Array` (`* "\n"`), `TSV`
|
|
468
|
+
(`data.dumper_stream`) or `TSV::Dumper` (`data.stream`); `nil` is tolerated
|
|
469
|
+
(produce.rb:114-126). Because the `case` evaluates `when TSV` before `when nil`
|
|
470
|
+
(produce.rb:119, 123), a proc returning `nil` in a process where no `TSV` constant
|
|
471
|
+
is defined raises `NameError: uninitialized constant Resource::TSV` — verified P43.
|
|
472
|
+
When a TSV library *is* loaded, the ordering still means a String/IO result is fine
|
|
473
|
+
but any other non-listed object raises `"Unkown object produced: …"` (sic,
|
|
474
|
+
produce.rb:125);
|
|
475
|
+
- `:rake` → `run_rake(path, content, rake_dir)`; failures whose message contains
|
|
476
|
+
`"Don't know how to build task"` are converted to `ResourceNotFound`
|
|
477
|
+
(produce.rb:127-136);
|
|
478
|
+
- `:install` → `software_dir = self.root.software`, `Resource.install(content, name,
|
|
479
|
+
software_dir)`, then `set_software_env(software_dir)` (produce.rb:137-141);
|
|
480
|
+
- anything else raises `"Could not produce #{resource}. (#{type}, #{content})"`
|
|
481
|
+
(produce.rb:142-143).
|
|
482
|
+
The implemented claim types are therefore **`:string`, `:url`, `:proc`, `:rake`,
|
|
483
|
+
`:install`** (plus `:csv` which is a stub). There is no `:annotation` claim type.
|
|
484
|
+
5. After production the path's location cache is invalidated —
|
|
485
|
+
`path.instance_variable_set("@path", {})` (produce.rb:155) — so that a subsequent
|
|
486
|
+
`find` re-scans all maps (e.g. picking up a `.gz` that just appeared); `path` is
|
|
487
|
+
returned (produce.rb:157).
|
|
488
|
+
|
|
489
|
+
### 3.5 `lib/scout/resource/produce/rake.rb` — ScoutRake
|
|
490
|
+
|
|
491
|
+
- `Rake::FileTask.define_task` is monkey-patched to record every defined file task in a
|
|
492
|
+
class-level `@@files` list with `Rake::FileTask.files` / `.clear_files`
|
|
493
|
+
(rake.rb:5-23).
|
|
494
|
+
- `module ScoutRake` defines `TaskNotFound < StandardError` (rake.rb:26) and
|
|
495
|
+
`ScoutRake.run(rakefile, dir, task, &block)` (rake.rb:27-68):
|
|
496
|
+
clears `Rake::Task` and the recorded files (rake.rb:30-31), then **forks**. In the
|
|
497
|
+
child: if a block is given it is `instance_exec`'d on the top-level binding receiver
|
|
498
|
+
(rake.rb:36); else the rakefile is either loaded from disk (`rakefile.produce.find`
|
|
499
|
+
first, rake.rb:38-40) or written to a `TmpFile` and loaded (rake.rb:42-45). A missing
|
|
500
|
+
task raises `TaskNotFound` (rake.rb:48); the task is invoked inside `Misc.in_dir(dir)`
|
|
501
|
+
(rake.rb:52), any exception is logged (`Log.exception`, `Log.error`) and the child
|
|
502
|
+
`Kernel.exit!(-1)` (rake.rb:58-62); success exits 0 (rake.rb:63). The parent
|
|
503
|
+
`Misc.wait_child(pid)` and raises `"Rake failed"` unless `$?.success?` (rake.rb:65-66).
|
|
504
|
+
Net effect: rake production happens in a separate process; rake-side exceptions do not
|
|
505
|
+
propagate as-is — the parent sees a generic `"Rake failed"` (which the
|
|
506
|
+
`:rake` arm in produce.rb:130-135 inspects for the "Don't know how to build task"
|
|
507
|
+
text to convert to `ResourceNotFound`).
|
|
508
|
+
|
|
509
|
+
### 3.6 `lib/scout/resource/open.rb` — auto-produce on Open
|
|
510
|
+
|
|
511
|
+
Reopens `class << Open` aliasing the original `open` as `_just_open` and defining
|
|
512
|
+
`Open.open(file, *args, **kwargs, &block)` that calls `file.produce` when `file` is a
|
|
513
|
+
`Path` before delegating (open.rb:1-9). Combined with `Path#find`'s transparent
|
|
514
|
+
alternative lookup this is why `Open.open(some_unlocated_path)` materialises resources.
|
|
515
|
+
|
|
516
|
+
### 3.7 `lib/scout/resource/util.rb` — identify / relocate
|
|
517
|
+
|
|
518
|
+
`Resource#identify(path)` (util.rb:2-68) — the inverse of `follow`:
|
|
519
|
+
- non-Path inputs are `Path.setup`; located-ness short-circuits to the input itself
|
|
520
|
+
(util.rb:3-4);
|
|
521
|
+
- uses the path's (or the resource's, or the global) `path_maps`, minus `:current`
|
|
522
|
+
(util.rb:6-11), and `Path.caller_lib_dir` for `{LIBDIR}` (util.rb:13);
|
|
523
|
+
- each map pattern is turned into an anchored regexp: `{TOPLEVEL}` → `(?<TOPLEVEL>[^/]+)`,
|
|
524
|
+
`\.{PKGDIR}` → `\.(?<PKGDIR>[^/]+)` (note the leading dot — matching the `.{PKGDIR}`
|
|
525
|
+
form used by the `:user` map), `{LIBDIR}` substituted literally, any other
|
|
526
|
+
`{GROUP}` → an optional `(?<GROUP>[^/]+)` segment, plus a trailing optional
|
|
527
|
+
`(?<REST>.+)` and optional slash (util.rb:28-34);
|
|
528
|
+
- a match is accepted only when the pattern had no PKGDIR group or the captured pkgdir
|
|
529
|
+
equals `self.final_pkgdir` (util.rb:35-36); the unlocated form is assembled from the
|
|
530
|
+
first non-nil of `TOPLEVEL/SUBPATH/PATH/REST` (util.rb:38-40), the resource `subdir`
|
|
531
|
+
is stripped as a prefix (util.rb:42-46), the result is annotated (util.rb:48) and
|
|
532
|
+
collected as a candidate;
|
|
533
|
+
- the shortest candidate wins (util.rb:55), falling back to the input (util.rb:57);
|
|
534
|
+
- `$HOME` is collapsed to `~` (util.rb:59-63) and the result is re-setup with the
|
|
535
|
+
resource and its path maps (util.rb:65).
|
|
536
|
+
|
|
537
|
+
`Resource.identify(path)` (util.rb:70-75) resolves the resource from `path.pkgdir`,
|
|
538
|
+
falling back to `Resource.default_resource`; `Resource.relocate(path)` (util.rb:77-81)
|
|
539
|
+
returns the path when it exists, else `identify(path).find`.
|
|
540
|
+
|
|
541
|
+
### 3.8 `lib/scout/resource/software.rb` — `:install` claims
|
|
542
|
+
|
|
543
|
+
- `Resource.install_helpers` → `Scout.share.software.install_helpers.find(:lib)`
|
|
544
|
+
(software.rb:3-5), i.e. the helper shell library shipped in the gem's `share` tree.
|
|
545
|
+
- `Resource.install(content, name, software_dir = Path.setup('software'), &block)`
|
|
546
|
+
(software.rb:7-97): the software dir is resolved with `find(:user)` when it is a Path
|
|
547
|
+
(software.rb:9); a block overrides the content; a Hash content may carry `:name`,
|
|
548
|
+
`:git`, `:src`, `:url`, `:jar`, `:extra`, `:commands`; a remote String is classified as
|
|
549
|
+
`{:git => …}` when it matches `git:|\.git$` else `{:src => …}` (software.rb:24-30).
|
|
550
|
+
The generated bash script is a fixed preamble (`SOFTWARE_DIR`, sourcing
|
|
551
|
+
`INSTALL_HELPER_FILE`, software.rb:13-20) followed by `install_git`/`install_src`/
|
|
552
|
+
`install_jar`/raw-commands sections (software.rb:32-91), executed via
|
|
553
|
+
`CMD.cmd_log('bash', :in => script)` (software.rb:95); `Resource.set_software_env` is
|
|
554
|
+
called afterwards (software.rb:96).
|
|
555
|
+
- `Resource.set_software_env(software_dir = Path.setup('software'))` (software.rb:99-175):
|
|
556
|
+
iterates `software_dir.opt.find_all` (all map locations) in reverse; for each existing
|
|
557
|
+
location it adds `<dir>/opt/bin` to `PATH`, touches `.ld-paths/.c-paths/.pkgconfig-paths/
|
|
558
|
+
.aclocal-paths/.java-classpaths` under `opt` (software.rb:108-119), then reads each of
|
|
559
|
+
those files to extend `C_INCLUDE_PATH`/`CPLUS_INCLUDE_PATH`, `LIBRARY_PATH`/
|
|
560
|
+
`LD_LIBRARY_PATH`/`LD_RUN_PATH`, `PKG_CONFIG_PATH`, `ACLOCAL_FLAGS`, `CLASSPATH`, plus
|
|
561
|
+
every `opt/jars/*.jar` (software.rb:121-156); finally, any `opt/.post_install/*` files
|
|
562
|
+
are scanned for `export VAR=value` lines whose `$var` references are expanded from
|
|
563
|
+
`ENV` and applied (software.rb:158-173). The module calls `self.set_software_env` at
|
|
564
|
+
require time (software.rb:177).
|
|
565
|
+
|
|
566
|
+
### 3.9 `lib/scout/resource/sync.rb`
|
|
567
|
+
|
|
568
|
+
`Resource.sync(path, map = nil, options = {})` (sync.rb:3-23): `map` defaults to
|
|
569
|
+
`'user'`; the resource comes from `options[:resource]`, else `path.pkgdir` when it is a
|
|
570
|
+
Resource, else `Resource.default_resource` (sync.rb:4-9). The target is
|
|
571
|
+
`resource.identify(path).find(map)` (sync.rb:11). Source selection: the literal path when
|
|
572
|
+
it exists, else `path.directory? ? path.find_all : path.glob_all` (sync.rb:13-18); each
|
|
573
|
+
source is copied with `Open.sync(source, target, options)` (sync.rb:20-22), which is an
|
|
574
|
+
alias of `Open.rsync` (open/sync.rb:81-83) — rsync `-avztHP --copy-unsafe-links
|
|
575
|
+
--omit-dir-times` with the default excludes `.save .crap .source tmp filecache
|
|
576
|
+
open-remote` (open/sync.rb:7, 42-44), `--link-dest` when `:hard_link` (open/sync.rb:43),
|
|
577
|
+
`-nv` when `:test`, optional `--files-from` (open/sync.rb:45-51), plus optional post-hoc
|
|
578
|
+
deletion when `:delete && :files` (open/sync.rb:66-79).
|
|
579
|
+
|
|
580
|
+
---
|
|
581
|
+
|
|
582
|
+
## 4. Annotation subsystem
|
|
583
|
+
|
|
584
|
+
### 4.1 `lib/scout/annotation.rb` — the module-level API
|
|
585
|
+
|
|
586
|
+
- `Annotation.setup(obj, annotation_types, annotation_hash)` (annotation.rb:7-20):
|
|
587
|
+
`annotation_types` may be a `"|"`-joined String, a single type or an Array; each name is
|
|
588
|
+
resolved with `Kernel.const_get` and applied via `type.setup(obj, annotation_hash)`
|
|
589
|
+
(annotation.rb:9-14); unknown names are only warned about (`Log.warn "Annotation #{type}
|
|
590
|
+
not defined"`, annotation.rb:15-16) and skipped. `nil` input returns nil (annotation.rb:8).
|
|
591
|
+
- `Annotation.extended(base)` (annotation.rb:22-26): when a module/class `extend`s
|
|
592
|
+
`Annotation` it gets `@annotations = []`, `include Annotation::AnnotatedObject` and
|
|
593
|
+
`extend Annotation::AnnotationModule` — this is what turns a plain module into an
|
|
594
|
+
annotation *type* with `annotation`, `setup`, `annotated?`, …
|
|
595
|
+
- `Annotation.is_annotated?(obj)` (annotation.rb:28-30): checks
|
|
596
|
+
`obj.instance_variables.include?(:@annotation_types) && obj.respond_to?(:purge)`.
|
|
597
|
+
- `Annotation.purge(obj)` (annotation.rb:32-48): recursively strips annotations from
|
|
598
|
+
Arrays (purging the array itself when annotated, then each element), Hashes (keys and
|
|
599
|
+
values) and annotated leaves (`obj.purge`).
|
|
600
|
+
|
|
601
|
+
### 4.2 `lib/scout/annotation/annotated_object.rb` — instance side
|
|
602
|
+
|
|
603
|
+
Mixed into every annotation *type*, so into every annotated object:
|
|
604
|
+
- `#annotation_types` → memoised `@annotation_types ||= []` (annotated_object.rb:3-5) —
|
|
605
|
+
populated by `AnnotationModule#extended` (annotation_module.rb:29);
|
|
606
|
+
- `#base_type` → `annotation_types.last` (annotated_object.rb:7-9);
|
|
607
|
+
- `#annotation_hash` → `{name => ivar}` for every name in `@annotations`
|
|
608
|
+
(annotated_object.rb:11-17);
|
|
609
|
+
- `#annotation_info` → `annotation_hash.merge(annotation_types:, annotated_array:)`
|
|
610
|
+
(annotated_object.rb:19-21);
|
|
611
|
+
- `AnnotatedObject.serialize(obj)` / `#serialize` (annotated_object.rb:23-29):
|
|
612
|
+
`Annotation.purge(obj.annotation_info.merge(literal: obj))` — a *plain Hash*
|
|
613
|
+
`{<attr> => value, annotation_types: [...], annotated_array: bool, literal: obj}`
|
|
614
|
+
(verified P42). There is no TSV involved at this level;
|
|
615
|
+
- `#annotation_id` / `#id` → `Misc.digest([self, annotation_info])` (annotated_object.rb:31-35);
|
|
616
|
+
- `#annotate(other)` → apply every own annotation type (with the own attribute values) to
|
|
617
|
+
another object (annotated_object.rb:37-42);
|
|
618
|
+
- `#purge` (annotated_object.rb:44-73): `dup`, then remove every `@<attr>` listed in
|
|
619
|
+
`@annotations` plus `@annotations`, `@annotation_types`, `@container`,
|
|
620
|
+
`@container_index`, and recursively purge every remaining ivar value (annotated_object.rb:67-70).
|
|
621
|
+
The *class* of the object is unchanged — a purged annotated Array is still an Array
|
|
622
|
+
extended with the annotation modules, just without the metadata (verified: after purge
|
|
623
|
+
`Annotation.is_annotated?` is false because `@annotation_types` is gone, but
|
|
624
|
+
`instance_variables` shows the annotated-array state was removed, P36g);
|
|
625
|
+
- `#make_array` → `[self]`, annotated and extended with `AnnotatedArray`
|
|
626
|
+
(annotated_object.rb:75-80).
|
|
627
|
+
|
|
628
|
+
### 4.3 `lib/scout/annotation/annotation_module.rb` — type definition
|
|
629
|
+
|
|
630
|
+
- `#annotation(*attrs)` (annotation_module.rb:4-11): appends new names to the type's
|
|
631
|
+
`@annotations` (skipping duplicates) and defines attr accessors.
|
|
632
|
+
- `#annotations` → memoised `@annotations ||= []` (annotation_module.rb:13-15).
|
|
633
|
+
- `#included(mod)` / `#extended(obj)` (annotation_module.rb:17-34): propagate the
|
|
634
|
+
attribute list; `extended` additionally pushes `self` onto `obj.annotation_types`
|
|
635
|
+
(annotation_module.rb:29) — this is the only place `@annotation_types` is grown, and it
|
|
636
|
+
explains `Annotation.is_annotated?`'s ivar check.
|
|
637
|
+
- **`#setup(*args, &block)`** (annotation_module.rb:36-69): the workhorse used as
|
|
638
|
+
`MyType.setup(obj, …)`. A leading block is treated as the object (`obj, rest = block,
|
|
639
|
+
args`, annotation_module.rb:37-41); frozen objects are dup'ed (annotation_module.rb:44);
|
|
640
|
+
`obj.extend self` is attempted and a `TypeError` (extending a non-module-friendly
|
|
641
|
+
singleton, e.g. some immediates) simply returns the object un-annotated
|
|
642
|
+
(annotation_module.rb:45-49). Argument binding (annotation_module.rb:54-61): if the last
|
|
643
|
+
rest arg is a 1-element Hash whose first key is an attribute name — or, loosely, when
|
|
644
|
+
the attribute list is not of length 1 (`! attrs.length != 1`, a condition that is true
|
|
645
|
+
whenever `attrs.length != 1` and false when it is exactly 1) — the Hash is used as
|
|
646
|
+
name/value pairs; otherwise attributes are zipped positionally with the rest args.
|
|
647
|
+
Values are assigned as `@<name>` ivars, skipping `nil` values and the reserved
|
|
648
|
+
`:annotation_types` name (annotation_module.rb:63-66).
|
|
649
|
+
Note the aliasing chain: `Path.setup` is exactly this method
|
|
650
|
+
(`Path.setup('a/b').annotation_types == [Path]`, P36f) because `Path` extends
|
|
651
|
+
`Annotation`; `Path.setup` therefore accepts either a Hash of `:pkgdir/:libdir/…` or
|
|
652
|
+
positional values in annotation declaration order (`pkgdir, libdir, path_maps, map_order,
|
|
653
|
+
where, original`, path.rb:8).
|
|
654
|
+
|
|
655
|
+
### 4.4 `lib/scout/annotation/array.rb` — AnnotatedArray
|
|
656
|
+
|
|
657
|
+
- `AnnotatedArrayItem` (array.rb:3-5) adds `container` / `container_index` accessors;
|
|
658
|
+
`AnnotatedArray.is_contained?(obj)` (array.rb:7-9) tests for it.
|
|
659
|
+
- `#annotate_item(obj, position = nil)` (array.rb:11-19): dup frozen items, extend nested
|
|
660
|
+
Arrays with `AnnotatedArray`, extend with `AnnotatedArrayItem`, record the container and
|
|
661
|
+
index, then `self.annotate(obj)`.
|
|
662
|
+
- Index/iteration overrides that lazily annotate items: `#[]` (with a `clean = false`
|
|
663
|
+
second arg to skip annotation, array.rb:21-25), `#first`, `#last`, `#each_with_index`,
|
|
664
|
+
`#each`, `#inject` (array.rb:27-63).
|
|
665
|
+
- `#collect` is implemented on top of `inject` so that both block and no-block forms work
|
|
666
|
+
(array.rb:65-71); `#select` builds a fresh annotated array (array.rb:41-48).
|
|
667
|
+
- Set-like helpers: `#subset(list)` = `self.annotate(self & list)`, `#remove(list)` =
|
|
668
|
+
`self.annotate(self - list)` (array.rb:73-79).
|
|
669
|
+
- `compact, uniq, flatten, reverse, sort_by` are wrapped so the result is re-annotated and
|
|
670
|
+
re-extended with `AnnotatedArray` (array.rb:81-91).
|
|
671
|
+
|
|
672
|
+
Container representation (settling the suspicion): an annotated *array* is the Array
|
|
673
|
+
object itself extended with `AnnotatedArray` (which in turn was `setup` with the
|
|
674
|
+
annotation types), carrying `@annotations`, `@annotation_types` and the per-attribute
|
|
675
|
+
ivars directly on the Array — **there is no separate container/name/value triplet
|
|
676
|
+
structure**. Items fetched out of an annotated array get `container`/`container_index`
|
|
677
|
+
plus a copy of the annotations via `annotate_item`; `Annotation.purge` removes all of it.
|
|
678
|
+
|
|
679
|
+
### 4.5 What `:annotation` persistence actually needs
|
|
680
|
+
|
|
681
|
+
`Persist.serialize(…, :annotation)` calls `Annotation.tsv(content, :all).to_s`
|
|
682
|
+
(serialize.rb:37-38) and `Persist.deserialize` calls
|
|
683
|
+
`Annotation.load_tsv(TSV.open(serialized))` (serialize.rb:74-75). Neither `Annotation.tsv`
|
|
684
|
+
nor `Annotation.load_tsv` nor `TSV` itself is defined anywhere in this gem (`grep`
|
|
685
|
+
`def tsv|load_tsv` under `lib/scout/annotation*` returns nothing; `Object.const_defined?(:TSV)`
|
|
686
|
+
is false, P42). The `:annotation` type is therefore **external-library dependent and
|
|
687
|
+
non-functional standalone** — `Persist.persist(…, :annotation, …)` raises
|
|
688
|
+
`NoMethodError: undefined method 'tsv' for module Annotation` (verified P38), and
|
|
689
|
+
`Resource`'s `:proc` claim dispatcher similarly trips over the `TSV` constant
|
|
690
|
+
(produce.rb:119-122, verified P43).
|
|
691
|
+
|
|
692
|
+
---
|
|
693
|
+
|
|
694
|
+
## 5. Cross-cutting notes and doc-suspicion hot-spots
|
|
695
|
+
|
|
696
|
+
1. **`Path#find` never returns nil.** Located paths return `self`; unlocated paths
|
|
697
|
+
fall through to `follow(:default)` (the `:user` map) even when nothing exists
|
|
698
|
+
(find.rb:247-274, P34). Docs claiming "returns nil if not found" are wrong.
|
|
699
|
+
2. **Default search order** is *derived*, not the `path_maps` declaration order, and
|
|
700
|
+
`:default`/`:tmp` trail at the end (find.rb:103-119, P34/P39). `basic_map_order`
|
|
701
|
+
contains a `workflow` entry with no default map.
|
|
702
|
+
3. **No `Path.map_order=`** — only `add_path`/`prepend_path`/`append_path` mutate it
|
|
703
|
+
(P39). Instance-level `path_maps=`/`map_order=` also do not exist; you assign the
|
|
704
|
+
per-instance copy via `p.path_maps = {...}` only because `path_maps` is a declared
|
|
705
|
+
annotation attr (path.rb:8) — there is no equivalent for `map_order` on instances
|
|
706
|
+
beyond `prepend_path`.
|
|
707
|
+
4. **`Persist.persistence_path(name, :marshal)` is invalid** (P41); the type belongs to
|
|
708
|
+
`Persist.persist`'s second positional parameter.
|
|
709
|
+
5. **`Persist` default serializer is `:json`** (serialize.rb:7), while `Persist.load`'s
|
|
710
|
+
`:serializer` branch uses `Open.marshal` (serialize.rb:144-145) — a mismatch to
|
|
711
|
+
double-check in docs: `load(file)` with no type marshals, `persist(name)` with no type
|
|
712
|
+
JSON-serialises.
|
|
713
|
+
6. **`Persist.deserialize(…, :yaml)` returns a Psych AST node** (serialize.rb:69, P35),
|
|
714
|
+
unlike `Persist.load(file, :yaml)` which returns real objects.
|
|
715
|
+
7. **`:annotation` persistence is unimplemented standalone** (serialize.rb:37-38, 74-75;
|
|
716
|
+
P38, P42); **`:csv` claims are a stub** that raises immediately (produce.rb:98-102, P37b).
|
|
717
|
+
8. **Implemented claim types**: `:string`, `:url`, `:proc`, `:rake`, `:install`
|
|
718
|
+
(produce.rb:95-141). `:proc` returning `nil` mis-fires on the undefined `TSV`
|
|
719
|
+
constant (produce.rb:119-123, P43).
|
|
720
|
+
9. **`Path#produce` memoisation uses `@produced` tri-state**: `true` on success/no-op,
|
|
721
|
+
`false` after `ResourceNotFound`, an Exception object stored and re-raised on later
|
|
722
|
+
calls (resource/path.rb:2-21, P37c).
|
|
723
|
+
10. **Extension fall-through is asymmetric**: `Resource#produce` retries `.gz` then `.bgz`
|
|
724
|
+
for unclaimed un-compressed names (produce.rb:64-73), so asking for `x` when `x.gz`
|
|
725
|
+
is claimed produces the gzip file and `find` then returns `x.gz`; asking for `x.gz`
|
|
726
|
+
when only `x` is claimed yields a `false` produce result (P37c).
|
|
727
|
+
11. **Produce locks** live in `Resource#lock_dir` (default `$HOME/.scout/tmp/produce_locks`)
|
|
728
|
+
and are named after the *final* path via TmpFile digest naming (produce.rb:87);
|
|
729
|
+
**persist locks** live in `Persist.lock_dir` (`$HOME/.scout/tmp/persist_locks`) with
|
|
730
|
+
a `.persist` suffix (persist.rb:61) — two distinct lock namespaces.
|
|
731
|
+
12. **Rake production is forked**; parent-side failures surface as the generic
|
|
732
|
+
`"Rake failed"` string which produce.rb:130-135 string-matches to convert
|
|
733
|
+
"Don't know how to build task" into `ResourceNotFound` (rake.rb:27-68).
|
|
734
|
+
13. **`Open.open` auto-produces Paths** (resource/open.rb:4-9), and `Open.yaml`/`Open.json`
|
|
735
|
+
transparently use `find_with_extension` (persist/open.rb:7, 12).
|
|
736
|
+
14. **`Resource.identify` collapses `$HOME` to `~`** (util.rb:59-63) and strips the
|
|
737
|
+
resource `subdir` prefix (util.rb:42-46); its regexp treats `.{PKGDIR}` (dot-prefixed)
|
|
738
|
+
specially (util.rb:31).
|
|
739
|
+
15. **`Path#digest_str` wraps non-existent paths in literal single quotes**
|
|
740
|
+
(digest.rb:20) — the output is `'<path>'`, quotes included.
|
|
741
|
+
16. **`Annotation.setup` swallows unknown type names with a warning** (annotation.rb:15-16),
|
|
742
|
+
and `AnnotationModule#setup` returns un-annotated objects when `extend` raises
|
|
743
|
+
`TypeError` (annotation_module.rb:47-49).
|
|
744
|
+
17. **`AnnotatedArray#[]`'s second parameter is `clean`**, not a length: `arr[0, true]`
|
|
745
|
+
returns the raw un-annotated item (array.rb:21-25).
|
|
746
|
+
|
|
747
|
+
---
|
|
748
|
+
|
|
749
|
+
## Files covered checklist
|
|
750
|
+
|
|
751
|
+
- [x] lib/scout/path.rb
|
|
752
|
+
- [x] lib/scout/path/digest.rb
|
|
753
|
+
- [x] lib/scout/path/find.rb
|
|
754
|
+
- [x] lib/scout/path/tmpfile.rb
|
|
755
|
+
- [x] lib/scout/path/util.rb
|
|
756
|
+
- [x] lib/scout/persist.rb
|
|
757
|
+
- [x] lib/scout/persist/open.rb
|
|
758
|
+
- [x] lib/scout/persist/path.rb
|
|
759
|
+
- [x] lib/scout/persist/serialize.rb
|
|
760
|
+
- [x] lib/scout/resource.rb
|
|
761
|
+
- [x] lib/scout/resource/open.rb
|
|
762
|
+
- [x] lib/scout/resource/path.rb
|
|
763
|
+
- [x] lib/scout/resource/produce.rb
|
|
764
|
+
- [x] lib/scout/resource/produce/rake.rb
|
|
765
|
+
- [x] lib/scout/resource/scout.rb
|
|
766
|
+
- [x] lib/scout/resource/software.rb
|
|
767
|
+
- [x] lib/scout/resource/sync.rb
|
|
768
|
+
- [x] lib/scout/resource/util.rb
|
|
769
|
+
- [x] lib/scout/annotation.rb
|
|
770
|
+
- [x] lib/scout/annotation/annotated_object.rb
|
|
771
|
+
- [x] lib/scout/annotation/annotation_module.rb
|
|
772
|
+
- [x] lib/scout/annotation/array.rb
|
|
773
|
+
|
|
774
|
+
(22/22.)
|