invar 0.9.0 → 0.9.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c7bb218e77d7ce1acc82bf0160cc7386529f7e32e5a56cd2eea608bd2a191ad6
4
- data.tar.gz: 6b209fa6041be75736d28783d996c8fe0ee1e529bdde7d94dcb8e1bbda23423d
3
+ metadata.gz: 2346259fc26689ffca5386f39e982de60dc9f2d9c03399f85023e05a47802f7a
4
+ data.tar.gz: b6e2cea0c52fd0b52eb833036844c03d3a714d83e0572dc5dcbb4ae083c61cf7
5
5
  SHA512:
6
- metadata.gz: da15b60c94678b1dc9a5e002b3f4bba02d7b2e1a77923483a2a5427839b0cff0fd0e137ce9e1ca847e6b493bb134be97f419a20bef7903adabe8de8b0f163ae9
7
- data.tar.gz: 2d60ab3a7f5374478fe2ab872083ef4159620097e0cb65575eb6e6956b669c9fceaa86a39c085c637df125cebfc33f9ba52170a1a2389dce713de9d77da09c9d
6
+ metadata.gz: 1a9556b30d2c17fda9373551ecf6fffeaacabd35aa0e40bb883c78e8bd94642bf4bebbeea054b5f9a9681b0bb23b6ee8c30860afb732f94b9d7887e62e0c0c47
7
+ data.tar.gz: a80954a8dbfc4c42d3f57b94cb2830a8e598eda85cd95f37ff431a1ab2b253fa64d4c65f9aacba8cfa718f5b5f1082b05f78c0d1f97d2ae7bc58b4392c4e19f0
data/RELEASE_NOTES.md CHANGED
@@ -19,6 +19,21 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
19
19
 
20
20
  * none
21
21
 
22
+ ## [0.9.1] - 2023-09-30
23
+
24
+ ### Major Changes
25
+
26
+ * none
27
+
28
+ ### Minor Changes
29
+
30
+ * none
31
+
32
+ ### Bugfixes
33
+
34
+ * Improved handling of test-only methods when trying to access them via `#method`
35
+ * Fixed TTY detection
36
+
22
37
  ## [0.9.0] - 2023-09-24
23
38
 
24
39
  ### Major Changes
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'namespaced'
4
+
5
+ module Invar
6
+ module Rake
7
+ module Task
8
+ # Rake task handler for actions to do with configuration files
9
+ class ConfigFileHandler < NamespacedFileTask
10
+ # Creates a config file in the appropriate location
11
+ def create
12
+ config_dir.mkpath
13
+ file_path.write CONFIG_TEMPLATE
14
+ file_path.chmod 0o600
15
+
16
+ warn "Created file: #{ file_path }"
17
+ end
18
+
19
+ # Edits the existing config file in the appropriate location
20
+ def edit
21
+ content = $stdin.stat.pipe? ? $stdin.read : nil
22
+ file_path = configs_file
23
+
24
+ if content
25
+ file_path.write content
26
+ else
27
+ system ENV.fetch('EDITOR', 'editor'), file_path.to_s, exception: true
28
+ end
29
+
30
+ warn "File saved to: #{ file_path }"
31
+ end
32
+
33
+ private
34
+
35
+ def configs_file
36
+ @locator.find 'config.yml'
37
+ rescue ::Invar::FileLocator::FileNotFoundError => e
38
+ warn <<~ERR
39
+ Abort: #{ e.message }. Searched in: #{ @locator.search_paths.join(', ') }
40
+ #{ CREATE_SUGGESTION }
41
+ ERR
42
+ exit 1
43
+ end
44
+
45
+ def filename
46
+ 'config.yml'
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../tasks'
4
+
5
+ module Invar
6
+ module Rake
7
+ module Task
8
+ # Abstract class for tasks that use a namespace for file searching
9
+ class NamespacedFileTask
10
+ def initialize(namespace)
11
+ @locator = FileLocator.new(namespace)
12
+ end
13
+
14
+ def file_path
15
+ config_dir / filename
16
+ end
17
+
18
+ private
19
+
20
+ def config_dir
21
+ @locator.search_paths.first
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'namespaced'
4
+
5
+ module Invar
6
+ module Rake
7
+ module Task
8
+ # Rake task handler for actions on the secrets file.
9
+ class SecretsFileHandler < NamespacedFileTask
10
+ # Instructions hint for how to handle secret keys.
11
+ SECRETS_INSTRUCTIONS = <<~INST
12
+ Generated key. Save this key to a secure password manager, you will need it to edit the secrets.yml file:
13
+ INST
14
+
15
+ SWAP_EXT = 'tmp'
16
+
17
+ # Creates a new encrypted secrets file and prints the generated encryption key to STDOUT
18
+ def create(content: SECRETS_TEMPLATE)
19
+ encryption_key = Lockbox.generate_key
20
+
21
+ write_encrypted_file(file_path,
22
+ encryption_key: encryption_key,
23
+ content: content,
24
+ permissions: PrivateFile::DEFAULT_PERMISSIONS)
25
+
26
+ warn SECRETS_INSTRUCTIONS
27
+ puts encryption_key
28
+ end
29
+
30
+ # Updates the file with new content.
31
+ #
32
+ # Either the content is provided over STDIN or the default editor is opened with the decrypted contents of
33
+ # the secrets file. After closing the editor, the file will be updated with the new encrypted contents.
34
+ def edit
35
+ content = $stdin.stat.pipe? ? $stdin.read : nil
36
+
37
+ edit_encrypted_file(secrets_file, content: content)
38
+
39
+ warn "File saved to #{ secrets_file }"
40
+ end
41
+
42
+ def rotate
43
+ file_path = secrets_file
44
+
45
+ decrypted = read_encrypted_file(file_path, encryption_key: determine_key(file_path))
46
+
47
+ swap_file = file_path.dirname / [file_path.basename, SWAP_EXT].join('.')
48
+ file_path.rename swap_file
49
+
50
+ begin
51
+ create content: decrypted
52
+ swap_file.delete
53
+ rescue StandardError
54
+ swap_file.rename file_path.to_s
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def secrets_file
61
+ @locator.find 'secrets.yml'
62
+ rescue ::Invar::FileLocator::FileNotFoundError => e
63
+ warn <<~ERR
64
+ Abort: #{ e.message }. Searched in: #{ @locator.search_paths.join(', ') }
65
+ #{ CREATE_SUGGESTION }
66
+ ERR
67
+ exit 1
68
+ end
69
+
70
+ def filename
71
+ 'secrets.yml'
72
+ end
73
+
74
+ def read_encrypted_file(file_path, encryption_key:)
75
+ lockbox = build_lockbox(encryption_key)
76
+ lockbox.decrypt(file_path.binread)
77
+ end
78
+
79
+ def write_encrypted_file(file_path, encryption_key:, content:, permissions: nil)
80
+ lockbox = build_lockbox(encryption_key)
81
+
82
+ encrypted_data = lockbox.encrypt(content)
83
+
84
+ config_dir.mkpath
85
+ # TODO: replace File.opens with photo_path.binwrite(uri.data) once FakeFS can handle it
86
+ File.open(file_path.to_s, 'wb') { |f| f.write encrypted_data }
87
+ file_path.chmod permissions if permissions
88
+
89
+ warn "Saved file: #{ file_path }"
90
+ end
91
+
92
+ def edit_encrypted_file(file_path, content: nil)
93
+ encryption_key = determine_key(file_path)
94
+
95
+ content ||= invoke_editor(file_path, encryption_key: encryption_key)
96
+
97
+ write_encrypted_file(file_path, encryption_key: encryption_key, content: content)
98
+ end
99
+
100
+ def invoke_editor(file_path, encryption_key:)
101
+ Tempfile.create(file_path.basename.to_s) do |tmp_file|
102
+ decrypted = read_encrypted_file(file_path, encryption_key: encryption_key)
103
+
104
+ tmp_file.write(decrypted)
105
+ tmp_file.rewind # rewind needed because file does not get closed after write
106
+ system ENV.fetch('EDITOR', 'editor'), tmp_file.path, exception: true
107
+ tmp_file.read
108
+ end
109
+ end
110
+
111
+ def determine_key(file_path)
112
+ encryption_key = Lockbox.master_key
113
+
114
+ if encryption_key.nil? && $stdin.tty?
115
+ warn "Enter master key to decrypt #{ file_path }:"
116
+ encryption_key = $stdin.noecho(&:gets).strip
117
+ end
118
+
119
+ encryption_key
120
+ end
121
+
122
+ def build_lockbox(encryption_key)
123
+ Lockbox.new(key: encryption_key)
124
+ rescue ArgumentError => e
125
+ raise SecretsFileEncryptionError, e
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'namespaced'
4
+
5
+ module Invar
6
+ module Rake
7
+ module Task
8
+ # Rake task handler for actions that just show information about the system
9
+ class StatusHandler < NamespacedFileTask
10
+ # Prints the current paths to be searched in
11
+ def show_paths
12
+ warn @locator.search_paths.join("\n")
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -3,29 +3,24 @@
3
3
  require 'invar'
4
4
 
5
5
  require 'rake'
6
- require 'io/console'
7
6
  require 'tempfile'
8
7
 
8
+ require_relative 'task/config'
9
+ require_relative 'task/secrets'
10
+ require_relative 'task/status'
11
+
9
12
  module Invar
10
- # Rake task implementation.
13
+ # Rake task module for Invar-related tasks.
14
+ #
15
+ # The specific rake task implementations are delegated to handlers in Invar::Rake::Task
11
16
  #
12
- # The actual rake tasks themselves are thinly defined in invar/rake.rb (so that the external include
13
- # path is nice and short)
17
+ # @see Invar::Rake::Tasks.define
14
18
  module Rake
15
19
  # RakeTask builder class. Use Tasks.define to generate the needed tasks.
16
20
  class Tasks
17
21
  include ::Rake::Cloneable
18
22
  include ::Rake::DSL
19
23
 
20
- # Template config YAML file
21
- CONFIG_TEMPLATE = SECRETS_TEMPLATE = <<~YML
22
- ---
23
- YML
24
-
25
- CREATE_SUGGESTION = <<~SUGGESTION
26
- Maybe you used the wrong namespace or need to create the file with bundle exec rake invar:init?
27
- SUGGESTION
28
-
29
24
  # Shorthand for Invar::Rake::Tasks.new.define
30
25
  #
31
26
  # @param (see #define)
@@ -62,8 +57,8 @@ module Invar
62
57
  task :init, [:mode] do |_task, args|
63
58
  mode = args.mode
64
59
 
65
- config = ::Invar::Rake::Tasks::ConfigFileHandler.new(app_namespace)
66
- secrets = ::Invar::Rake::Tasks::SecretsFileHandler.new(app_namespace)
60
+ config = ::Invar::Rake::Task::ConfigFileHandler.new(app_namespace)
61
+ secrets = ::Invar::Rake::Task::SecretsFileHandler.new(app_namespace)
67
62
 
68
63
  case mode
69
64
  when 'config'
@@ -84,7 +79,7 @@ module Invar
84
79
  def define_config_task(app_namespace)
85
80
  desc 'Edit the config in your default editor'
86
81
  task :configs do
87
- ::Invar::Rake::Tasks::ConfigFileHandler.new(app_namespace).edit
82
+ ::Invar::Rake::Task::ConfigFileHandler.new(app_namespace).edit
88
83
  end
89
84
 
90
85
  # alias
@@ -94,7 +89,7 @@ module Invar
94
89
  def define_secrets_tasks(app_namespace)
95
90
  desc 'Edit the encrypted secrets file in your default editor'
96
91
  task :secrets do
97
- ::Invar::Rake::Tasks::SecretsFileHandler.new(app_namespace).edit
92
+ ::Invar::Rake::Task::SecretsFileHandler.new(app_namespace).edit
98
93
  end
99
94
 
100
95
  # alias
@@ -102,14 +97,14 @@ module Invar
102
97
 
103
98
  desc 'Encrypt the secrets file with a new generated key'
104
99
  task :rotate do
105
- ::Invar::Rake::Tasks::SecretsFileHandler.new(app_namespace).rotate
100
+ ::Invar::Rake::Task::SecretsFileHandler.new(app_namespace).rotate
106
101
  end
107
102
  end
108
103
 
109
104
  def define_info_tasks(app_namespace)
110
105
  desc 'Show directories to be searched for the given namespace'
111
106
  task :paths do
112
- ::Invar::Rake::Tasks::StatusHandler.new(app_namespace).show_paths
107
+ ::Invar::Rake::Task::StatusHandler.new(app_namespace).show_paths
113
108
  end
114
109
  end
115
110
 
@@ -136,194 +131,18 @@ module Invar
136
131
  warn msg
137
132
  exit 1
138
133
  end
134
+ end
139
135
 
140
- # Tasks that use a namespace for file searching
141
- class NamespacedFileTask
142
- def initialize(namespace)
143
- @locator = FileLocator.new(namespace)
144
- end
145
-
146
- def file_path
147
- config_dir / filename
148
- end
149
-
150
- private
151
-
152
- def config_dir
153
- @locator.search_paths.first
154
- end
155
- end
156
-
157
- # Configuration file actions.
158
- class ConfigFileHandler < NamespacedFileTask
159
- # Creates a config file in the appropriate location
160
- def create
161
- config_dir.mkpath
162
- file_path.write CONFIG_TEMPLATE
163
- file_path.chmod 0o600
164
-
165
- warn "Created file: #{ file_path }"
166
- end
167
-
168
- # Edits the existing config file in the appropriate location
169
- def edit
170
- content = $stdin.tty? ? nil : $stdin.read
171
- file_path = configs_file
172
-
173
- if content
174
- file_path.write content
175
- else
176
- system ENV.fetch('EDITOR', 'editor'), file_path.to_s, exception: true
177
- end
178
-
179
- warn "File saved to: #{ file_path }"
180
- end
181
-
182
- private
183
-
184
- def configs_file
185
- @locator.find 'config.yml'
186
- rescue ::Invar::FileLocator::FileNotFoundError => e
187
- warn <<~ERR
188
- Abort: #{ e.message }. Searched in: #{ @locator.search_paths.join(', ') }
189
- #{ CREATE_SUGGESTION }
190
- ERR
191
- exit 1
192
- end
193
-
194
- def filename
195
- 'config.yml'
196
- end
197
- end
198
-
199
- # Secrets file actions.
200
- class SecretsFileHandler < NamespacedFileTask
201
- # Instructions hint for how to handle secret keys.
202
- SECRETS_INSTRUCTIONS = <<~INST
203
- Generated key. Save this key to a secure password manager, you will need it to edit the secrets.yml file:
204
- INST
205
-
206
- SWAP_EXT = 'tmp'
207
-
208
- # Creates a new encrypted secrets file and prints the generated encryption key to STDOUT
209
- def create(content: SECRETS_TEMPLATE)
210
- encryption_key = Lockbox.generate_key
211
-
212
- write_encrypted_file(file_path,
213
- encryption_key: encryption_key,
214
- content: content,
215
- permissions: PrivateFile::DEFAULT_PERMISSIONS)
216
-
217
- warn SECRETS_INSTRUCTIONS
218
- puts encryption_key
219
- end
220
-
221
- # Updates the file with new content.
222
- #
223
- # Either the content is provided over STDIN or the default editor is opened with the decrypted contents of
224
- # the secrets file. After closing the editor, the file will be updated with the new encrypted contents.
225
- def edit
226
- content = $stdin.tty? ? nil : $stdin.read
227
-
228
- edit_encrypted_file(secrets_file, content: content)
229
-
230
- warn "File saved to #{ secrets_file }"
231
- end
232
-
233
- def rotate
234
- file_path = secrets_file
235
-
236
- decrypted = read_encrypted_file(file_path, encryption_key: determine_key(file_path))
237
-
238
- swap_file = file_path.dirname / [file_path.basename, SWAP_EXT].join('.')
239
- file_path.rename swap_file
240
-
241
- begin
242
- create content: decrypted
243
- swap_file.delete
244
- rescue StandardError
245
- swap_file.rename file_path.to_s
246
- end
247
- end
248
-
249
- private
250
-
251
- def secrets_file
252
- @locator.find 'secrets.yml'
253
- rescue ::Invar::FileLocator::FileNotFoundError => e
254
- warn <<~ERR
255
- Abort: #{ e.message }. Searched in: #{ @locator.search_paths.join(', ') }
256
- #{ CREATE_SUGGESTION }
257
- ERR
258
- exit 1
259
- end
260
-
261
- def filename
262
- 'secrets.yml'
263
- end
264
-
265
- def read_encrypted_file(file_path, encryption_key:)
266
- lockbox = build_lockbox(encryption_key)
267
- lockbox.decrypt(file_path.binread)
268
- end
269
-
270
- def write_encrypted_file(file_path, encryption_key:, content:, permissions: nil)
271
- lockbox = build_lockbox(encryption_key)
272
-
273
- encrypted_data = lockbox.encrypt(content)
274
-
275
- config_dir.mkpath
276
- # TODO: replace File.opens with photo_path.binwrite(uri.data) once FakeFS can handle it
277
- File.open(file_path.to_s, 'wb') { |f| f.write encrypted_data }
278
- file_path.chmod permissions if permissions
279
-
280
- warn "Saved file: #{ file_path }"
281
- end
282
-
283
- def edit_encrypted_file(file_path, content: nil)
284
- encryption_key = determine_key(file_path)
285
-
286
- content ||= invoke_editor(file_path, encryption_key: encryption_key)
287
-
288
- write_encrypted_file(file_path, encryption_key: encryption_key, content: content)
289
- end
290
-
291
- def invoke_editor(file_path, encryption_key:)
292
- Tempfile.create(file_path.basename.to_s) do |tmp_file|
293
- decrypted = read_encrypted_file(file_path, encryption_key: encryption_key)
294
-
295
- tmp_file.write(decrypted)
296
- tmp_file.rewind # rewind needed because file does not get closed after write
297
- system(ENV.fetch('EDITOR', 'editor'), tmp_file.path, exception: true)
298
- tmp_file.read
299
- end
300
- end
301
-
302
- def determine_key(file_path)
303
- encryption_key = Lockbox.master_key
304
-
305
- if encryption_key.nil? && $stdin.respond_to?(:noecho)
306
- warn "Enter master key to decrypt #{ file_path }:"
307
- encryption_key = $stdin.noecho(&:gets).strip
308
- end
309
-
310
- encryption_key
311
- end
312
-
313
- def build_lockbox(encryption_key)
314
- Lockbox.new(key: encryption_key)
315
- rescue ArgumentError => e
316
- raise SecretsFileEncryptionError, e
317
- end
318
- end
136
+ # Namespace module for task handler implementations
137
+ module Task
138
+ # Template config YAML file
139
+ CONFIG_TEMPLATE = SECRETS_TEMPLATE = <<~YML
140
+ ---
141
+ YML
319
142
 
320
- # General status tasks
321
- class StatusHandler < NamespacedFileTask
322
- # Prints the current paths to be searched in
323
- def show_paths
324
- warn @locator.search_paths.join("\n")
325
- end
326
- end
143
+ CREATE_SUGGESTION = <<~SUGGESTION
144
+ Maybe you used the wrong namespace or need to create the file with bundle exec rake invar:init?
145
+ SUGGESTION
327
146
  end
328
147
  end
329
148
  end
data/lib/invar/reality.rb CHANGED
@@ -152,11 +152,9 @@ module Invar
152
152
  end
153
153
 
154
154
  def resolve_key(pathname, locator, prompt)
155
- key_file = locator.find(pathname)
156
-
157
- read_keyfile(key_file)
155
+ read_keyfile locator.find pathname
158
156
  rescue FileLocator::FileNotFoundError
159
- if $stdin.respond_to?(:noecho)
157
+ if $stdin.tty?
160
158
  warn prompt
161
159
  $stdin.noecho(&:gets).strip
162
160
  else
data/lib/invar/scope.rb CHANGED
@@ -25,12 +25,18 @@ module Invar
25
25
  alias / fetch
26
26
  alias [] fetch
27
27
 
28
- def method_missing(symbol, *args)
29
- raise ::Invar::ImmutableRealityError, ::Invar::ImmutableRealityError::PRETEND_MSG if symbol == :pretend
28
+ def method_missing(method_name, *args)
29
+ guard_test_methods method_name
30
30
 
31
31
  super
32
32
  end
33
33
 
34
+ def respond_to_missing?(method_name, include_all)
35
+ guard_test_methods method_name
36
+
37
+ super method_name, include_all
38
+ end
39
+
34
40
  # Returns a hash representation of this scope and subscopes.
35
41
  #
36
42
  # @return [Hash] a hash representation of this scope
@@ -51,6 +57,10 @@ module Invar
51
57
 
52
58
  private
53
59
 
60
+ def guard_test_methods(method_name)
61
+ raise ::Invar::ImmutableRealityError, ::Invar::ImmutableRealityError::PRETEND_MSG if method_name == :pretend
62
+ end
63
+
54
64
  def known_keys
55
65
  @data.keys.sort.collect { |k| ":#{ k }" }.join(', ')
56
66
  end
data/lib/invar/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Invar
4
4
  # Current version of the gem
5
- VERSION = '0.9.0'
5
+ VERSION = '0.9.1'
6
6
  end
data/lib/invar.rb CHANGED
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Needed for TTY input handling
4
+ # Do not remove; it missing will not always break tests due to test environments requiring it themselves
5
+ require 'io/console'
6
+
3
7
  require_relative 'invar/version'
4
8
  require_relative 'invar/errors'
5
9
  require_relative 'invar/reality'
@@ -15,12 +19,24 @@ module Invar
15
19
  end
16
20
 
17
21
  class << self
18
- def method_missing(meth)
19
- if [:after_load, :clear_hooks].include? meth
20
- raise ::Invar::ImmutableRealityError, ::Invar::ImmutableRealityError::HOOK_MSG
21
- end
22
+ def method_missing(method_name)
23
+ guard_test_hooks method_name
22
24
 
23
25
  super
24
26
  end
27
+
28
+ def respond_to_missing?(method_name, include_all)
29
+ guard_test_hooks method_name
30
+
31
+ super method_name, include_all
32
+ end
33
+
34
+ private
35
+
36
+ def guard_test_hooks(method_name)
37
+ return unless [:after_load, :clear_hooks].include? method_name
38
+
39
+ raise ::Invar::ImmutableRealityError, ::Invar::ImmutableRealityError::HOOK_MSG
40
+ end
25
41
  end
26
42
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: invar
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.9.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robin Miller
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-09-25 00:00:00.000000000 Z
11
+ date: 2023-09-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dry-schema
@@ -63,6 +63,10 @@ files:
63
63
  - lib/invar/errors.rb
64
64
  - lib/invar/file_locator.rb
65
65
  - lib/invar/private_file.rb
66
+ - lib/invar/rake/task/config.rb
67
+ - lib/invar/rake/task/namespaced.rb
68
+ - lib/invar/rake/task/secrets.rb
69
+ - lib/invar/rake/task/status.rb
66
70
  - lib/invar/rake/tasks.rb
67
71
  - lib/invar/reality.rb
68
72
  - lib/invar/scope.rb