boxwerk 0.1.0 → 0.3.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.
data/lib/boxwerk/setup.rb CHANGED
@@ -1,70 +1,157 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Boxwerk
4
- # Setup orchestrates the Boxwerk boot process
5
4
  module Setup
6
5
  class << self
7
- # Main entry point for setup
8
- # @param start_dir [String] Directory to start searching for package.yml
9
- # @return [Boxwerk::Graph] The loaded and validated graph
10
- def run!(start_dir: Dir.pwd)
11
- # Find the root package.yml
12
- root_path = find_package_yml(start_dir)
13
- unless root_path
14
- raise 'Cannot find package.yml in current directory or ancestors'
15
- end
6
+ def run(start_dir: Dir.pwd, packages: nil, config: {})
7
+ root_path = find_root(start_dir)
8
+
9
+ resolver = Boxwerk::PackageResolver.new(root_path, config_overrides: config)
10
+ config = resolver.boxwerk_config
11
+ eager_load_global = config.fetch('eager_load_global', true)
12
+ eager_load_packages = config.fetch('eager_load_packages', false)
16
13
 
17
- # Build and validate dependency graph (happens automatically in constructor)
18
- graph = Boxwerk::Graph.new(root_path)
14
+ # Create GlobalContext and expose it as Boxwerk.global before boot.rb runs
15
+ global_context = GlobalContext.new(root_path)
16
+ root_box = Ruby::Box.root
17
+ root_box.__send__(:remove_const, :BOXWERK_GLOBAL) if root_box.const_defined?(:BOXWERK_GLOBAL, false)
18
+ root_box.const_set(:BOXWERK_GLOBAL, global_context)
19
19
 
20
- # Create a registry instance for tracking booted packages
21
- registry = Boxwerk::Registry.new
20
+ # Run global boot in root box (after gems, before package boxes).
21
+ run_global_boot(root_path, eager_load: eager_load_global, global_context: global_context)
22
22
 
23
- # Boot all packages in topological order (all in isolated boxes)
24
- Boxwerk::Loader.boot_all(graph, registry)
23
+ # Eager-load all Zeitwerk-managed constants in root box so child
24
+ # boxes inherit fully resolved constants (not pending autoloads).
25
+ eager_load_zeitwerk if eager_load_global
26
+
27
+ @box_manager = Boxwerk::BoxManager.new(root_path)
28
+ if packages
29
+ packages.each do |pkg|
30
+ @box_manager.boot_package(
31
+ pkg,
32
+ resolver,
33
+ eager_load_packages: eager_load_packages,
34
+ )
35
+ end
36
+ else
37
+ @box_manager.boot_all(
38
+ resolver,
39
+ eager_load_packages: eager_load_packages,
40
+ )
41
+ end
25
42
 
26
- # Store graph for introspection
27
- @graph = graph
43
+ check_gem_conflicts(@box_manager.gem_resolver, resolver)
44
+
45
+ @resolver = resolver
28
46
  @booted = true
29
47
 
30
- graph
48
+ { resolver: resolver, box_manager: @box_manager, root_path: root_path }
49
+ end
50
+
51
+ def resolver
52
+ @resolver
31
53
  end
32
54
 
33
- # Returns the loaded graph (for introspection)
34
- # @return [Boxwerk::Graph, nil]
35
- def graph
36
- @graph
55
+ def box_manager
56
+ @box_manager
37
57
  end
38
58
 
39
- # Check if Boxwerk has been booted
40
- # @return [Boolean]
41
59
  def booted?
42
60
  @booted || false
43
61
  end
44
62
 
45
- # Reset the setup state (useful for testing)
46
- def reset!
47
- @graph = nil
63
+ def root_box
64
+ return nil unless @box_manager && @resolver&.root
65
+
66
+ @box_manager.boxes[@resolver.root.name]
67
+ end
68
+
69
+ def reset
70
+ @resolver = nil
71
+ @box_manager = nil
48
72
  @booted = false
73
+ if defined?(Ruby::Box)
74
+ root_box = Ruby::Box.root
75
+ root_box.__send__(:remove_const, :BOXWERK_GLOBAL) if root_box.const_defined?(:BOXWERK_GLOBAL, false)
76
+ end
49
77
  end
50
78
 
51
79
  private
52
80
 
53
- # Find package.yml by searching up the directory tree
54
- # @param start_dir [String] Directory to start searching from
55
- # @return [String, nil] Path to directory containing package.yml, or nil
56
- def find_package_yml(start_dir)
81
+ # Finds the project root directory. Walks up the directory tree
82
+ # looking for boxwerk.yml or package.yml. Falls back to start_dir
83
+ # if neither is found (implicit root).
84
+ def find_root(start_dir)
57
85
  current = File.expand_path(start_dir)
58
86
  loop do
59
- package_yml = File.join(current, 'package.yml')
60
- return current if File.exist?(package_yml)
87
+ return current if File.exist?(File.join(current, 'boxwerk.yml'))
88
+ return current if File.exist?(File.join(current, 'package.yml'))
61
89
 
62
90
  parent = File.dirname(current)
63
- break if parent == current # reached filesystem root
91
+ break if parent == current
64
92
 
65
93
  current = parent
66
94
  end
67
- nil
95
+
96
+ # Fall back to CWD as implicit root
97
+ File.expand_path(start_dir)
98
+ end
99
+
100
+ # Runs the optional global boot in the root box. The global/
101
+ # directory is autoloaded and global/boot.rb is required in the root
102
+ # box. These run after global gems are loaded but before package
103
+ # boxes are created, so definitions here are inherited by all boxes.
104
+ #
105
+ # Root-level boot.rb is NOT handled here — it runs in the root
106
+ # package box via BoxManager (like any other package boot.rb).
107
+ def run_global_boot(root_path, eager_load: true, global_context: nil)
108
+ root_box = Ruby::Box.root
109
+ global_dir = File.join(root_path, 'global')
110
+ global_boot = File.join(global_dir, 'boot.rb')
111
+ global_entries = nil
112
+
113
+ # Always register lazy autoloads for global/ dir so constants are
114
+ # accessible in boot.rb (autoload fires on access in root box context).
115
+ # This works regardless of eager_load_global so global constants can
116
+ # be used in boot.rb even when eager loading is disabled.
117
+ if File.directory?(global_dir)
118
+ global_context&.__send__(:record_scanned_dir, 'global/')
119
+ entries = ZeitwerkScanner.scan(global_dir)
120
+ global_entries = entries.reject { |e| e.file == global_boot }
121
+ ZeitwerkScanner.register_autoloads(root_box, global_entries) if global_entries.any?
122
+ end
123
+
124
+ # Run global/boot.rb in root box (always, regardless of eager_load)
125
+ root_box.require(global_boot) if File.exist?(global_boot)
126
+
127
+ # After boot.rb, register lazy autoloads for any dirs added via
128
+ # Boxwerk.global.autoloader.push_dir during boot.
129
+ global_context&.autoloader&.setup
130
+
131
+ # Eager-load AFTER boot.rb so boot scripts run first.
132
+ if eager_load
133
+ global_entries&.each { |e| root_box.require(e.file) if e.file }
134
+ global_context&.autoloader&.__send__(:eager_load!)
135
+ end
136
+ end
137
+
138
+ def check_gem_conflicts(gem_resolver, package_resolver)
139
+ conflicts = gem_resolver.check_conflicts(package_resolver)
140
+ conflicts.each do |c|
141
+ warn "Boxwerk: gem '#{c[:gem_name]}' is #{c[:package_version]} in #{c[:package]} " \
142
+ "but #{c[:global_version]} in global gems — both versions will be loaded into memory"
143
+ end
144
+ end
145
+
146
+ # Eager-load all Zeitwerk-managed constants (gem autoloads) in the root
147
+ # box. Child boxes created via Ruby::Box.new inherit a snapshot of the
148
+ # root box's constants. Zeitwerk autoloads are lazy — without eager
149
+ # loading, child boxes inherit pending autoload entries that may not
150
+ # resolve correctly across box boundaries.
151
+ def eager_load_zeitwerk
152
+ Ruby::Box.root.eval(<<~RUBY)
153
+ Zeitwerk::Loader.eager_load_all if defined?(Zeitwerk)
154
+ RUBY
68
155
  end
69
156
  end
70
157
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Boxwerk
4
- VERSION = '0.1.0'
4
+ VERSION = '0.3.0'
5
5
  end
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zeitwerk'
4
+
5
+ module Boxwerk
6
+ # Uses Zeitwerk's file system scanner and inflector to discover constants
7
+ # in a directory. Zeitwerk's autoload registration cannot be used directly
8
+ # inside Ruby::Box (autoloads register in the box where the code was
9
+ # defined, not where it's called), so we only use Zeitwerk for:
10
+ #
11
+ # - File discovery (respecting Zeitwerk conventions: hidden dirs, etc.)
12
+ # - Inflection (file names → constant names)
13
+ #
14
+ # The actual autoload registration is done by BoxManager using box.eval.
15
+ module ZeitwerkScanner
16
+ Entry = Data.define(:type, :cname, :full_path, :file, :parent, :dir)
17
+
18
+ # Scans a directory and returns an array of Entry structs describing
19
+ # the constants and namespaces found. Uses a temporary Zeitwerk::Loader
20
+ # for its FileSystem scanner and Inflector.
21
+ def self.scan(dir)
22
+ loader = Zeitwerk::Loader.new
23
+ loader.push_dir(dir)
24
+ inflector = loader.inflector
25
+ fs = Zeitwerk::Loader::FileSystem.new(loader)
26
+
27
+ entries = []
28
+ scan_dir(fs, inflector, dir, '', entries)
29
+ entries
30
+ end
31
+
32
+ # Registers autoloads in a Ruby::Box based on scan results.
33
+ # Implicit namespaces become Module.new, explicit namespaces are
34
+ # eagerly loaded so child autoloads can attach to them.
35
+ def self.register_autoloads(box, entries)
36
+ namespaces = entries.select { |e| e.type == :namespace }
37
+ files = entries.select { |e| e.type == :file }
38
+
39
+ # Deduplicate namespaces: when both lib/ and public/ contribute a
40
+ # namespace with the same full_path, prefer the explicit one (has a
41
+ # .rb file) so the module definition is loaded rather than replaced
42
+ # by an empty Module.new.
43
+ namespaces =
44
+ namespaces
45
+ .group_by(&:full_path)
46
+ .values
47
+ .map { |group| group.find { |ns| ns.file } || group.first }
48
+
49
+ # Phase 1: Set up namespaces
50
+ namespaces.each do |ns|
51
+ if ns.file
52
+ # Explicit namespace: autoload the .rb file
53
+ register_autoload(box, ns.parent, ns.cname, ns.file)
54
+ else
55
+ # Implicit namespace: create empty module
56
+ define_implicit_module(box, ns.parent, ns.cname, ns.full_path)
57
+ end
58
+ end
59
+
60
+ # Phase 2: Eagerly trigger explicit namespaces so children can attach
61
+ namespaces.each { |ns| box.eval(ns.full_path) if ns.file }
62
+
63
+ # Phase 3: Register file autoloads
64
+ files.each { |f| register_autoload(box, f.parent, f.cname, f.file) }
65
+ end
66
+
67
+ # Scans files in a collapsed directory, computing the parent namespace
68
+ # from the path between root_dir and the collapsed dir. E.g. if root_dir
69
+ # is lib/ and dir is lib/analytics/formatters/, files map to Analytics::*.
70
+ def self.scan_files_only(dir, root_dir: nil)
71
+ inflector = Zeitwerk::Inflector.new
72
+ entries = []
73
+
74
+ parent_cnames =
75
+ if root_dir && dir.start_with?("#{root_dir}/")
76
+ rel = dir.delete_prefix("#{root_dir}/")
77
+ rel.split('/')[0...-1].map { |p| inflector.camelize(p, root_dir) }
78
+ else
79
+ []
80
+ end
81
+
82
+ Dir
83
+ .glob(File.join(dir, '**', '*.rb'))
84
+ .sort
85
+ .each do |abspath|
86
+ relative = abspath.delete_prefix("#{dir}/").delete_suffix('.rb')
87
+ parts = relative.split('/')
88
+ file_cnames = parts.map { |part| inflector.camelize(part, dir) }
89
+ all_cnames = parent_cnames + file_cnames
90
+ full_path = all_cnames.join('::')
91
+ cname = all_cnames.last
92
+ parent = all_cnames[0...-1].join('::')
93
+
94
+ entries << Entry.new(
95
+ type: :file,
96
+ cname: cname,
97
+ full_path: full_path,
98
+ file: abspath,
99
+ parent: parent,
100
+ dir: nil,
101
+ )
102
+ end
103
+
104
+ entries
105
+ end
106
+
107
+ # Builds a file index (const_name → file_path) from scan entries.
108
+ # Used by ConstantResolver for dependency wiring.
109
+ def self.build_file_index(entries)
110
+ index = {}
111
+ entries.each { |e| index[e.full_path] = e.file if e.file }
112
+ index
113
+ end
114
+
115
+ class << self
116
+ private
117
+
118
+ def scan_dir(fs, inflector, dir, parent_path, entries)
119
+ fs.ls(dir) do |basename, abspath, ftype|
120
+ if ftype == :file
121
+ # Skip files that have a matching directory (explicit namespaces).
122
+ # These are already handled as namespace entries with their .rb file.
123
+ next if File.directory?(abspath.delete_suffix('.rb'))
124
+
125
+ cname = inflector.camelize(basename.delete_suffix('.rb'), dir)
126
+ full_path = parent_path.empty? ? cname : "#{parent_path}::#{cname}"
127
+ entries << Entry.new(
128
+ type: :file,
129
+ cname: cname,
130
+ full_path: full_path,
131
+ file: abspath,
132
+ parent: parent_path,
133
+ dir: nil,
134
+ )
135
+ elsif ftype == :directory
136
+ cname = inflector.camelize(basename, dir)
137
+ full_path = parent_path.empty? ? cname : "#{parent_path}::#{cname}"
138
+ rb_file = "#{abspath}.rb"
139
+ has_rb = File.exist?(rb_file)
140
+ entries << Entry.new(
141
+ type: :namespace,
142
+ cname: cname,
143
+ full_path: full_path,
144
+ file: has_rb ? rb_file : nil,
145
+ parent: parent_path,
146
+ dir: abspath,
147
+ )
148
+ scan_dir(fs, inflector, abspath, full_path, entries)
149
+ end
150
+ end
151
+ end
152
+
153
+ def register_autoload(box, parent, cname, file)
154
+ if parent.empty?
155
+ box.eval("autoload :#{cname}, #{file.inspect}")
156
+ else
157
+ box.eval("#{parent}.autoload(:#{cname}, #{file.inspect})")
158
+ end
159
+ end
160
+
161
+ def define_implicit_module(box, parent, cname, full_path)
162
+ if parent.empty?
163
+ box.eval("#{cname} = Module.new unless defined?(#{cname})")
164
+ else
165
+ box.eval(
166
+ "#{parent}.const_set(:#{cname}, Module.new) unless defined?(#{full_path})",
167
+ )
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
data/lib/boxwerk.rb CHANGED
@@ -1,14 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'boxwerk/autoloader_mixin'
4
+ require_relative 'boxwerk/box_manager'
3
5
  require_relative 'boxwerk/cli'
4
- require_relative 'boxwerk/graph'
5
- require_relative 'boxwerk/loader'
6
+ require_relative 'boxwerk/constant_resolver'
7
+ require_relative 'boxwerk/gem_resolver'
8
+ require_relative 'boxwerk/gemfile_require_parser'
9
+ require_relative 'boxwerk/global_context'
6
10
  require_relative 'boxwerk/package'
7
- require_relative 'boxwerk/registry'
11
+ require_relative 'boxwerk/package_context'
12
+ require_relative 'boxwerk/package_resolver'
13
+ require_relative 'boxwerk/privacy_checker'
8
14
  require_relative 'boxwerk/setup'
9
15
  require_relative 'boxwerk/version'
16
+ require_relative 'boxwerk/zeitwerk_scanner'
10
17
 
18
+ # Boxwerk is a package isolation system for Ruby applications built on
19
+ # Ruby::Box. It loads each package in its own +Ruby::Box+, enforcing
20
+ # dependency and privacy boundaries declared in +package.yml+ files.
11
21
  module Boxwerk
12
- class Error < StandardError
22
+ class << self
23
+ # Returns the {PackageContext} for the currently executing package.
24
+ # Returns +nil+ in the root box.
25
+ # @return [PackageContext, nil]
26
+ def package
27
+ nil
28
+ end
29
+
30
+ # Returns the {GlobalContext}.
31
+ # @return [GlobalContext]
32
+ def global
33
+ Ruby::Box.root.const_get(:BOXWERK_GLOBAL)
34
+ end
13
35
  end
14
36
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: boxwerk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Cristofaro
@@ -9,24 +9,54 @@ bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: bundler
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '4.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '4.0'
12
26
  - !ruby/object:Gem::Dependency
13
27
  name: irb
14
28
  requirement: !ruby/object:Gem::Requirement
15
29
  requirements:
16
30
  - - "~>"
17
31
  - !ruby/object:Gem::Version
18
- version: '1.16'
32
+ version: '1.17'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.17'
40
+ - !ruby/object:Gem::Dependency
41
+ name: zeitwerk
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '2.7'
19
47
  type: :runtime
20
48
  prerelease: false
21
49
  version_requirements: !ruby/object:Gem::Requirement
22
50
  requirements:
23
51
  - - "~>"
24
52
  - !ruby/object:Gem::Version
25
- version: '1.16'
26
- description: Boxwerk provides strict package isolation for Ruby applications using
27
- Ruby 4.0+ Box. It is used to organize code into packages with explicit dependency
28
- graphs and strict access to constants between packages. It is inspired by Packwerk,
29
- a static package system.
53
+ version: '2.7'
54
+ description: Boxwerk is a tool for creating modular Ruby and Rails applications. It
55
+ organizes code into packages with clear boundaries and explicit dependencies, enforcing
56
+ them at runtime using Ruby::Box constant isolation. It reads standard Packwerk package.yml
57
+ files (without requiring Packwerk), providing per-package gem isolation, Zeitwerk-based
58
+ autoloading, monkey patch isolation between packages, and a CLI for running, testing,
59
+ and inspecting your modular application.
30
60
  email:
31
61
  - david@dtcristo.com
32
62
  executables:
@@ -34,31 +64,30 @@ executables:
34
64
  extensions: []
35
65
  extra_rdoc_files: []
36
66
  files:
67
+ - AGENTS.md
68
+ - ARCHITECTURE.md
37
69
  - CHANGELOG.md
38
70
  - LICENSE.txt
39
71
  - README.md
40
72
  - Rakefile
41
- - example/Gemfile
42
- - example/Gemfile.lock
43
- - example/README.md
44
- - example/app.rb
45
- - example/package.yml
46
- - example/packages/finance/lib/invoice.rb
47
- - example/packages/finance/lib/tax_calculator.rb
48
- - example/packages/finance/package.yml
49
- - example/packages/util/lib/calculator.rb
50
- - example/packages/util/lib/geometry.rb
51
- - example/packages/util/package.yml
73
+ - TODO.md
74
+ - USAGE.md
52
75
  - exe/boxwerk
53
76
  - lib/boxwerk.rb
77
+ - lib/boxwerk/autoloader_mixin.rb
78
+ - lib/boxwerk/box_manager.rb
54
79
  - lib/boxwerk/cli.rb
55
- - lib/boxwerk/graph.rb
56
- - lib/boxwerk/loader.rb
80
+ - lib/boxwerk/constant_resolver.rb
81
+ - lib/boxwerk/gem_resolver.rb
82
+ - lib/boxwerk/gemfile_require_parser.rb
83
+ - lib/boxwerk/global_context.rb
57
84
  - lib/boxwerk/package.rb
58
- - lib/boxwerk/registry.rb
85
+ - lib/boxwerk/package_context.rb
86
+ - lib/boxwerk/package_resolver.rb
87
+ - lib/boxwerk/privacy_checker.rb
59
88
  - lib/boxwerk/setup.rb
60
89
  - lib/boxwerk/version.rb
61
- - sig/boxwerk.rbs
90
+ - lib/boxwerk/zeitwerk_scanner.rb
62
91
  homepage: https://github.com/dtcristo/boxwerk
63
92
  licenses:
64
93
  - MIT
@@ -67,6 +96,7 @@ metadata:
67
96
  homepage_uri: https://github.com/dtcristo/boxwerk
68
97
  source_code_uri: https://github.com/dtcristo/boxwerk
69
98
  changelog_uri: https://github.com/dtcristo/boxwerk/blob/main/CHANGELOG.md
99
+ documentation_uri: https://dtcristo.github.io/boxwerk/
70
100
  rdoc_options: []
71
101
  require_paths:
72
102
  - lib
@@ -74,7 +104,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
74
104
  requirements:
75
105
  - - ">="
76
106
  - !ruby/object:Gem::Version
77
- version: 4.0.0
107
+ version: '4.0'
78
108
  required_rubygems_version: !ruby/object:Gem::Requirement
79
109
  requirements:
80
110
  - - ">="
@@ -83,5 +113,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
83
113
  requirements: []
84
114
  rubygems_version: 4.0.3
85
115
  specification_version: 4
86
- summary: Ruby package system with Box-powered constant isolation
116
+ summary: Ruby package system with Box-powered boundary enforcement
87
117
  test_files: []
data/example/Gemfile DELETED
@@ -1,6 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- source 'https://rubygems.org'
4
-
5
- gem 'boxwerk', path: '..'
6
- gem 'money'
data/example/Gemfile.lock DELETED
@@ -1,66 +0,0 @@
1
- PATH
2
- remote: ..
3
- specs:
4
- boxwerk (0.1.0)
5
- irb (~> 1.16)
6
-
7
- GEM
8
- remote: https://rubygems.org/
9
- specs:
10
- bigdecimal (4.0.1)
11
- concurrent-ruby (1.3.6)
12
- date (3.5.1)
13
- erb (6.0.1)
14
- i18n (1.14.8)
15
- concurrent-ruby (~> 1.0)
16
- io-console (0.8.2)
17
- irb (1.16.0)
18
- pp (>= 0.6.0)
19
- rdoc (>= 4.0.0)
20
- reline (>= 0.4.2)
21
- money (7.0.0)
22
- bigdecimal
23
- i18n (~> 1.9)
24
- pp (0.6.3)
25
- prettyprint
26
- prettyprint (0.2.0)
27
- psych (5.3.1)
28
- date
29
- stringio
30
- rdoc (7.0.3)
31
- erb
32
- psych (>= 4.0.0)
33
- tsort
34
- reline (0.6.3)
35
- io-console (~> 0.5)
36
- stringio (3.2.0)
37
- tsort (0.2.0)
38
-
39
- PLATFORMS
40
- arm64-darwin-25
41
- ruby
42
-
43
- DEPENDENCIES
44
- boxwerk!
45
- money
46
-
47
- CHECKSUMS
48
- bigdecimal (4.0.1) sha256=8b07d3d065a9f921c80ceaea7c9d4ae596697295b584c296fe599dd0ad01c4a7
49
- boxwerk (0.1.0)
50
- concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
51
- date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
52
- erb (6.0.1) sha256=28ecdd99c5472aebd5674d6061e3c6b0a45c049578b071e5a52c2a7f13c197e5
53
- i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
54
- io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
55
- irb (1.16.0) sha256=2abe56c9ac947cdcb2f150572904ba798c1e93c890c256f8429981a7675b0806
56
- money (7.0.0) sha256=6de776ee00aced8b9a435d3aac25ce8c600566625809b3d69bbe0c319e941dd5
57
- pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6
58
- prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
59
- psych (5.3.1) sha256=eb7a57cef10c9d70173ff74e739d843ac3b2c019a003de48447b2963d81b1974
60
- rdoc (7.0.3) sha256=dfe3d0981d19b7bba71d9dbaeb57c9f4e3a7a4103162148a559c4fc687ea81f9
61
- reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
62
- stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1
63
- tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
64
-
65
- BUNDLED WITH
66
- 4.0.3