en14960 0.2.3 → 0.4.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/.terragon-setup-complete +0 -0
- data/CHANGELOG.md +14 -0
- data/README.md +30 -0
- data/lib/en14960/api.rb +83 -0
- data/lib/en14960/calculators/anchor_calculator.rb +8 -1
- data/lib/en14960/calculators/slide_calculator.rb +18 -6
- data/lib/en14960/calculators/user_capacity_calculator.rb +11 -2
- data/lib/en14960/constants.rb +4 -0
- data/lib/en14960/models/calculator_response.rb +14 -5
- data/lib/en14960/source_code.rb +44 -12
- data/lib/en14960/validators/material_validator.rb +10 -6
- data/lib/en14960/validators/play_area_validator.rb +15 -11
- data/lib/en14960/version.rb +2 -1
- data/lib/en14960.rb +22 -1
- data/sorbet/config +6 -0
- data/terragon-setup.sh +34 -0
- metadata +48 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fda15828cb9885998e0aa06bb8cff2621e0dd3cbdd5ed5a717a102988f04ecb9
|
4
|
+
data.tar.gz: c9d0d2184b17fad322ca86a7ac19e978c7918ef1d2d412881072feb1ee995491
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bf879fdb5e594c2078c14959a2d2c1040afcefe051edfa42156b623fc0d66583ee48c8baf2ca81f91894cacbfee840deb4c7f4863cceb9260c927305a9332ed8
|
7
|
+
data.tar.gz: 73699ce3ae6651564d4641b6746e8819b57c3a0c8938990bd11b4783156bfa5f0fd339281eb00518d3abe23f2284b56f1a61de11612b6692e0d2b32fea3ffa1c
|
File without changes
|
data/CHANGELOG.md
CHANGED
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
|
+
## [Unreleased]
|
9
|
+
|
10
|
+
### Added
|
11
|
+
- Full Sorbet type signatures for all public APIs
|
12
|
+
- Runtime type checking with sorbet-runtime
|
13
|
+
- Strongly typed CalculatorResponse using T::Struct
|
14
|
+
- Type safety documentation in README
|
15
|
+
- Test file demonstrating type-safe usage
|
16
|
+
|
17
|
+
### Changed
|
18
|
+
- CalculatorResponse now extends T::Struct instead of using Data.define
|
19
|
+
- All modules and classes now include Sorbet type annotations
|
20
|
+
- Improved type safety for method parameters and return values
|
21
|
+
|
8
22
|
## [0.1.0] - 2025-01-28
|
9
23
|
|
10
24
|
### Added
|
data/README.md
CHANGED
@@ -10,6 +10,7 @@ A Ruby gem providing calculators and validators for BS EN 14960:2019 - the Briti
|
|
10
10
|
- **Slide Safety**: Calculate runout distances and wall height requirements
|
11
11
|
- **User Capacity**: Calculate safe user capacity based on play area and user heights
|
12
12
|
- **Material Validation**: Validate material specifications against EN 14960 requirements
|
13
|
+
- **Type Safety**: Full Sorbet type signatures for all public APIs ensuring type safety and better IDE support
|
13
14
|
|
14
15
|
## Installation
|
15
16
|
|
@@ -170,6 +171,35 @@ EN14960::Constants::MATERIAL_STANDARDS
|
|
170
171
|
EN14960::Constants::ANCHOR_CALCULATION_CONSTANTS
|
171
172
|
```
|
172
173
|
|
174
|
+
## Type Safety with Sorbet
|
175
|
+
|
176
|
+
This gem includes full Sorbet type signatures for improved type safety and IDE support:
|
177
|
+
|
178
|
+
```ruby
|
179
|
+
# All public methods have type signatures
|
180
|
+
sig { params(length: T.any(Float, Integer), width: T.any(Float, Integer), height: T.any(Float, Integer)).returns(CalculatorResponse) }
|
181
|
+
def calculate_anchors(length:, width:, height:)
|
182
|
+
# ...
|
183
|
+
end
|
184
|
+
|
185
|
+
# Return types are strongly typed
|
186
|
+
result = EN14960.calculate_anchors(length: 10, width: 8, height: 3)
|
187
|
+
result.value # Integer
|
188
|
+
result.value_suffix # String
|
189
|
+
result.breakdown # Array[Array[String]]
|
190
|
+
```
|
191
|
+
|
192
|
+
The gem uses `T::Struct` for data classes providing runtime type checking:
|
193
|
+
|
194
|
+
```ruby
|
195
|
+
# CalculatorResponse is a typed struct
|
196
|
+
class CalculatorResponse < T::Struct
|
197
|
+
const :value, T.any(Integer, Float, String, T::Array[T.untyped])
|
198
|
+
const :value_suffix, String, default: ""
|
199
|
+
const :breakdown, T::Array[T::Array[String]], default: []
|
200
|
+
end
|
201
|
+
```
|
202
|
+
|
173
203
|
## EN 14960:2019 Compliance
|
174
204
|
|
175
205
|
This gem implements calculations based on BS EN 14960:2019, including:
|
data/lib/en14960/api.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: strict
|
3
|
+
|
4
|
+
require "sorbet-runtime"
|
5
|
+
|
6
|
+
module EN14960
|
7
|
+
module API
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
class << self
|
11
|
+
extend T::Sig
|
12
|
+
|
13
|
+
sig { params(length: Float, width: Float, height: Float).returns(CalculatorResponse) }
|
14
|
+
def calculate_anchors(length:, width:, height:)
|
15
|
+
Calculators::AnchorCalculator.calculate(length: length, width: width, height: height)
|
16
|
+
end
|
17
|
+
|
18
|
+
sig { params(platform_height: Float, has_stop_wall: T::Boolean).returns(CalculatorResponse) }
|
19
|
+
def calculate_slide_runout(platform_height, has_stop_wall: false)
|
20
|
+
Calculators::SlideCalculator.calculate_required_runout(platform_height, has_stop_wall: has_stop_wall)
|
21
|
+
end
|
22
|
+
|
23
|
+
sig { params(platform_height: Float, user_height: Float, has_permanent_roof: T.nilable(T::Boolean)).returns(CalculatorResponse) }
|
24
|
+
def calculate_wall_height(platform_height, user_height, has_permanent_roof = nil)
|
25
|
+
Calculators::SlideCalculator.calculate_wall_height_requirements(
|
26
|
+
platform_height,
|
27
|
+
user_height,
|
28
|
+
has_permanent_roof
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
sig { params(length: Float, width: Float, max_user_height: T.nilable(Float), negative_adjustment_area: Float).returns(CalculatorResponse) }
|
33
|
+
def calculate_user_capacity(length, width, max_user_height = nil, negative_adjustment_area = 0.0)
|
34
|
+
Calculators::UserCapacityCalculator.calculate(
|
35
|
+
length,
|
36
|
+
width,
|
37
|
+
max_user_height,
|
38
|
+
negative_adjustment_area
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
sig { params(diameter_mm: Float).returns(T::Boolean) }
|
43
|
+
def valid_rope_diameter?(diameter_mm)
|
44
|
+
Validators::MaterialValidator.valid_rope_diameter?(diameter_mm)
|
45
|
+
end
|
46
|
+
|
47
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
48
|
+
def height_categories
|
49
|
+
Constants::HEIGHT_CATEGORIES
|
50
|
+
end
|
51
|
+
|
52
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
53
|
+
def material_standards
|
54
|
+
Constants::MATERIAL_STANDARDS
|
55
|
+
end
|
56
|
+
|
57
|
+
sig {
|
58
|
+
params(
|
59
|
+
unit_length: Float,
|
60
|
+
unit_width: Float,
|
61
|
+
play_area_length: Float,
|
62
|
+
play_area_width: Float,
|
63
|
+
negative_adjustment_area: Float
|
64
|
+
).returns(T::Hash[Symbol, T.untyped])
|
65
|
+
}
|
66
|
+
def validate_play_area(
|
67
|
+
unit_length:,
|
68
|
+
unit_width:,
|
69
|
+
play_area_length:,
|
70
|
+
play_area_width:,
|
71
|
+
negative_adjustment_area:
|
72
|
+
)
|
73
|
+
Validators::PlayAreaValidator.validate(
|
74
|
+
unit_length: unit_length,
|
75
|
+
unit_width: unit_width,
|
76
|
+
play_area_length: play_area_length,
|
77
|
+
play_area_width: play_area_width,
|
78
|
+
negative_adjustment_area: negative_adjustment_area
|
79
|
+
)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -1,19 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
# typed: strict
|
2
3
|
|
4
|
+
require "sorbet-runtime"
|
3
5
|
require_relative "../constants"
|
4
6
|
require_relative "../models/calculator_response"
|
5
7
|
|
6
8
|
module EN14960
|
7
9
|
module Calculators
|
8
10
|
module AnchorCalculator
|
11
|
+
extend T::Sig
|
9
12
|
extend self
|
10
13
|
|
14
|
+
sig { params(area_m2: Float).returns(Integer) }
|
11
15
|
def calculate_required_anchors(area_m2)
|
12
16
|
# EN 14960-1:2019 Annex A (Lines 1175-1210) - Anchor calculation formula
|
13
17
|
# Force = 0.5 × Cw × ρ × V² × A
|
14
18
|
# Where: Cw = 1.5, ρ = 1.24 kg/m³, V = 11.1 m/s (Lines 1194-1199)
|
15
19
|
# Number of anchors = Force / 1600N (Line 450 - each anchor withstands 1600N)
|
16
|
-
return 0 if area_m2
|
20
|
+
return 0 if area_m2 <= 0
|
17
21
|
|
18
22
|
# Pre-calculated: 0.5 × 1.5 × 1.24 × 11.1² ≈ 114
|
19
23
|
area_coeff = Constants::ANCHOR_CALCULATION_CONSTANTS[:area_coefficient]
|
@@ -23,6 +27,7 @@ module EN14960
|
|
23
27
|
((area_m2.to_f * area_coeff * safety_mult) / base_div).ceil
|
24
28
|
end
|
25
29
|
|
30
|
+
sig { params(length: Float, width: Float, height: Float).returns(CalculatorResponse) }
|
26
31
|
def calculate(length:, width:, height:)
|
27
32
|
# EN 14960-1:2019 Lines 1175-1210 (Annex A) - Calculate exposed surface areas
|
28
33
|
front_area = (width * height).round(1)
|
@@ -67,6 +72,7 @@ module EN14960
|
|
67
72
|
)
|
68
73
|
end
|
69
74
|
|
75
|
+
sig { returns(String) }
|
70
76
|
def anchor_formula_text
|
71
77
|
area_coeff = Constants::ANCHOR_CALCULATION_CONSTANTS[:area_coefficient]
|
72
78
|
base_div = Constants::ANCHOR_CALCULATION_CONSTANTS[:base_divisor]
|
@@ -74,6 +80,7 @@ module EN14960
|
|
74
80
|
"((Area × #{area_coeff} × #{safety_fact}) ÷ #{base_div})"
|
75
81
|
end
|
76
82
|
|
83
|
+
sig { returns(String) }
|
77
84
|
def anchor_calculation_description
|
78
85
|
"Anchors must be calculated based on the play area to ensure adequate ground restraint for wind loads."
|
79
86
|
end
|
@@ -1,16 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
# typed: strict
|
2
3
|
|
4
|
+
require "sorbet-runtime"
|
3
5
|
require_relative "../constants"
|
4
6
|
require_relative "../models/calculator_response"
|
5
7
|
|
6
8
|
module EN14960
|
7
9
|
module Calculators
|
8
10
|
module SlideCalculator
|
11
|
+
extend T::Sig
|
9
12
|
extend self
|
10
13
|
|
11
14
|
# Simple calculation method that returns just the numeric value
|
15
|
+
sig { params(platform_height: Float, has_stop_wall: T::Boolean).returns(Float) }
|
12
16
|
def calculate_runout_value(platform_height, has_stop_wall: false)
|
13
|
-
return 0 if platform_height
|
17
|
+
return 0.0 if platform_height <= 0
|
14
18
|
|
15
19
|
height_ratio = Constants::RUNOUT_CALCULATION_CONSTANTS[:platform_height_ratio]
|
16
20
|
minimum_runout = Constants::RUNOUT_CALCULATION_CONSTANTS[:minimum_runout_meters]
|
@@ -22,13 +26,14 @@ module EN14960
|
|
22
26
|
has_stop_wall ? base_runout + stop_wall_add : base_runout
|
23
27
|
end
|
24
28
|
|
29
|
+
sig { params(platform_height: Float, has_stop_wall: T::Boolean).returns(CalculatorResponse) }
|
25
30
|
def calculate_required_runout(platform_height, has_stop_wall: false)
|
26
31
|
# EN 14960-1:2019 Section 4.2.11 (Lines 930-939) - Runout requirements
|
27
32
|
# Line 934-935: The runout distance must be at least half the height of the slide's
|
28
33
|
# highest platform (measured from ground level), with an absolute minimum of 300mm
|
29
34
|
# Line 936: If a stop-wall is installed at the runout's end, an additional
|
30
35
|
# 50cm must be added to the total runout length
|
31
|
-
return CalculatorResponse.new(value: 0, value_suffix: "m", breakdown: []) if platform_height
|
36
|
+
return CalculatorResponse.new(value: 0, value_suffix: "m", breakdown: []) if platform_height <= 0
|
32
37
|
|
33
38
|
# Get constants
|
34
39
|
height_ratio = Constants::RUNOUT_CALCULATION_CONSTANTS[:platform_height_ratio]
|
@@ -59,13 +64,13 @@ module EN14960
|
|
59
64
|
)
|
60
65
|
end
|
61
66
|
|
67
|
+
sig { params(platform_height: Float, user_height: Float, containing_wall_height: Float, has_permanent_roof: T::Boolean).returns(T::Boolean) }
|
62
68
|
def meets_height_requirements?(platform_height, user_height, containing_wall_height, has_permanent_roof)
|
63
69
|
# EN 14960-1:2019 Section 4.2.9 (Lines 854-887) - Containment requirements
|
64
70
|
# Lines 859-860: Containing walls become mandatory for platforms exceeding 0.6m in height
|
65
71
|
# Lines 861-862: Platforms between 0.6m and 3.0m need walls at least as tall as the maximum user height
|
66
72
|
# Lines 863-864: Platforms between 3.0m and 6.0m require walls at least 1.25 times the maximum user height OR a permanent roof
|
67
73
|
# Lines 865-866: Platforms over 6.0m must have both containing walls and a permanent roof structure
|
68
|
-
return false if platform_height.nil? || user_height.nil? || containing_wall_height.nil? || has_permanent_roof.nil?
|
69
74
|
|
70
75
|
enhanced_multiplier = Constants::WALL_HEIGHT_CONSTANTS[:enhanced_height_multiplier]
|
71
76
|
thresholds = Constants::SLIDE_HEIGHT_THRESHOLDS
|
@@ -86,16 +91,17 @@ module EN14960
|
|
86
91
|
end
|
87
92
|
end
|
88
93
|
|
94
|
+
sig { params(runout_length: Float, platform_height: Float, has_stop_wall: T::Boolean).returns(T::Boolean) }
|
89
95
|
def meets_runout_requirements?(runout_length, platform_height, has_stop_wall: false)
|
90
96
|
# EN 14960-1:2019 Section 4.2.11 (Lines 930-939) - Runout requirements
|
91
97
|
# Lines 934-935: The runout area must extend at least half the platform's height
|
92
98
|
# or 300mm (whichever is greater) to allow users to decelerate safely
|
93
|
-
return false if runout_length.nil? || platform_height.nil?
|
94
99
|
|
95
100
|
required_runout = calculate_runout_value(platform_height, has_stop_wall: has_stop_wall)
|
96
101
|
runout_length >= required_runout
|
97
102
|
end
|
98
103
|
|
104
|
+
sig { returns(String) }
|
99
105
|
def slide_runout_formula_text
|
100
106
|
ratio_constant = Constants::RUNOUT_CALCULATION_CONSTANTS[:platform_height_ratio]
|
101
107
|
height_ratio = (ratio_constant * 100).to_i
|
@@ -104,9 +110,10 @@ module EN14960
|
|
104
110
|
"#{height_ratio}% of platform height, minimum #{min_runout}mm"
|
105
111
|
end
|
106
112
|
|
113
|
+
sig { params(platform_height: Float, user_height: Float, has_permanent_roof: T.nilable(T::Boolean)).returns(CalculatorResponse) }
|
107
114
|
def calculate_wall_height_requirements(platform_height, user_height, has_permanent_roof = nil)
|
108
115
|
# EN 14960-1:2019 Section 4.2.9 (Lines 854-887) - Containment requirements
|
109
|
-
return CalculatorResponse.new(value: 0, value_suffix: "m", breakdown: []) if platform_height
|
116
|
+
return CalculatorResponse.new(value: 0, value_suffix: "m", breakdown: []) if platform_height <= 0 || user_height <= 0
|
110
117
|
|
111
118
|
# Get requirement details and breakdown
|
112
119
|
requirement_details = get_wall_height_requirement_details(platform_height, user_height, has_permanent_roof)
|
@@ -121,6 +128,7 @@ module EN14960
|
|
121
128
|
)
|
122
129
|
end
|
123
130
|
|
131
|
+
sig { params(platform_height: Float, user_height: Float, has_permanent_roof: T.nilable(T::Boolean)).returns(T::Hash[Symbol, T.untyped]) }
|
124
132
|
def get_wall_height_requirement_details(platform_height, user_height, has_permanent_roof)
|
125
133
|
thresholds = Constants::SLIDE_HEIGHT_THRESHOLDS
|
126
134
|
enhanced_multiplier = Constants::WALL_HEIGHT_CONSTANTS[:enhanced_height_multiplier]
|
@@ -204,19 +212,22 @@ module EN14960
|
|
204
212
|
end
|
205
213
|
end
|
206
214
|
|
215
|
+
sig { params(platform_height: Float).returns(T::Boolean) }
|
207
216
|
def requires_permanent_roof?(platform_height)
|
208
217
|
# EN 14960-1:2019 Section 4.2.9 (Lines 865-866)
|
209
218
|
# Inflatable structures with platforms higher than 6.0m must be equipped
|
210
219
|
# with both containing walls and a permanent roof
|
211
220
|
threshold = Constants::SLIDE_HEIGHT_THRESHOLDS[:enhanced_walls]
|
212
|
-
|
221
|
+
platform_height > threshold
|
213
222
|
end
|
214
223
|
|
224
|
+
sig { returns(String) }
|
215
225
|
def wall_height_requirement
|
216
226
|
multiplier = Constants::WALL_HEIGHT_CONSTANTS[:enhanced_height_multiplier]
|
217
227
|
"Containing walls required #{multiplier} times user height"
|
218
228
|
end
|
219
229
|
|
230
|
+
sig { returns(T::Hash[Symbol, T::Hash[Symbol, String]]) }
|
220
231
|
def slide_calculations
|
221
232
|
# EN 14960:2019 - Comprehensive slide safety requirements
|
222
233
|
{
|
@@ -243,6 +254,7 @@ module EN14960
|
|
243
254
|
|
244
255
|
private
|
245
256
|
|
257
|
+
sig { params(platform_height: Float, user_height: Float).returns(Float) }
|
246
258
|
def extract_required_wall_height(platform_height, user_height)
|
247
259
|
thresholds = Constants::SLIDE_HEIGHT_THRESHOLDS
|
248
260
|
enhanced_multiplier = Constants::WALL_HEIGHT_CONSTANTS[:enhanced_height_multiplier]
|
@@ -1,19 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
# typed: strict
|
2
3
|
|
4
|
+
require "sorbet-runtime"
|
3
5
|
require_relative "../constants"
|
4
6
|
require_relative "../models/calculator_response"
|
5
7
|
|
6
8
|
module EN14960
|
7
9
|
module Calculators
|
8
10
|
module UserCapacityCalculator
|
11
|
+
extend T::Sig
|
9
12
|
extend self
|
10
13
|
|
14
|
+
sig { params(length: Float, width: Float, max_user_height: T.nilable(Float), negative_adjustment_area: Float).returns(CalculatorResponse) }
|
11
15
|
def calculate(length, width, max_user_height = nil, negative_adjustment_area = 0)
|
12
|
-
return default_result if length
|
16
|
+
return default_result if length <= 0 || width <= 0
|
13
17
|
|
14
18
|
total_area = (length * width).round(2)
|
15
19
|
negative_adjustment_area = negative_adjustment_area.to_f.abs
|
16
|
-
usable_area = [total_area - negative_adjustment_area, 0].max.round(2)
|
20
|
+
usable_area = [total_area - negative_adjustment_area, 0.0].max.round(2)
|
17
21
|
|
18
22
|
breakdown = build_breakdown(length, width, total_area, negative_adjustment_area, usable_area)
|
19
23
|
capacities = calculate_capacities(usable_area, max_user_height, breakdown)
|
@@ -27,6 +31,7 @@ module EN14960
|
|
27
31
|
|
28
32
|
private
|
29
33
|
|
34
|
+
sig { params(length: Float, width: Float, total_area: Float, negative_adjustment_area: Float, usable_area: Float).returns(T::Array[T::Array[String]]) }
|
30
35
|
def build_breakdown(length, width, total_area, negative_adjustment_area, usable_area)
|
31
36
|
breakdown = []
|
32
37
|
formatted_length = format_number(length)
|
@@ -47,6 +52,7 @@ module EN14960
|
|
47
52
|
breakdown
|
48
53
|
end
|
49
54
|
|
55
|
+
sig { params(usable_area: Float, max_user_height: T.nilable(Float), breakdown: T::Array[T::Array[String]]).returns(T::Hash[Symbol, Integer]) }
|
50
56
|
def calculate_capacities(usable_area, max_user_height, breakdown)
|
51
57
|
capacities = {}
|
52
58
|
|
@@ -71,6 +77,7 @@ module EN14960
|
|
71
77
|
capacities
|
72
78
|
end
|
73
79
|
|
80
|
+
sig { returns(CalculatorResponse) }
|
74
81
|
def default_result
|
75
82
|
CalculatorResponse.new(
|
76
83
|
value: default_capacity,
|
@@ -79,6 +86,7 @@ module EN14960
|
|
79
86
|
)
|
80
87
|
end
|
81
88
|
|
89
|
+
sig { returns(T::Hash[Symbol, Integer]) }
|
82
90
|
def default_capacity
|
83
91
|
{
|
84
92
|
users_1000mm: 0,
|
@@ -88,6 +96,7 @@ module EN14960
|
|
88
96
|
}
|
89
97
|
end
|
90
98
|
|
99
|
+
sig { params(number: Float).returns(String) }
|
91
100
|
def format_number(number)
|
92
101
|
# Remove trailing zeros after decimal point
|
93
102
|
formatted = sprintf("%.1f", number)
|
data/lib/en14960/constants.rb
CHANGED
@@ -1,7 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
# typed: strict
|
3
|
+
|
4
|
+
require "sorbet-runtime"
|
2
5
|
|
3
6
|
module EN14960
|
4
7
|
module Constants
|
8
|
+
extend T::Sig
|
5
9
|
# Height category constants based on EN 14960:2019
|
6
10
|
HEIGHT_CATEGORIES = {
|
7
11
|
1000 => {label: "1.0m (Young children)", max_users: :calculate_by_area},
|
@@ -1,13 +1,19 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
# typed: strict
|
3
|
+
|
4
|
+
require "sorbet-runtime"
|
2
5
|
|
3
6
|
module EN14960
|
4
7
|
# Data class for calculator responses
|
5
8
|
# Provides a consistent structure for all calculator results
|
6
|
-
CalculatorResponse
|
7
|
-
|
8
|
-
super
|
9
|
-
end
|
9
|
+
class CalculatorResponse < T::Struct
|
10
|
+
extend T::Sig
|
10
11
|
|
12
|
+
const :value, T.any(Integer, Float, String, T::Hash[Symbol, Integer], T::Array[T.untyped])
|
13
|
+
const :value_suffix, String, default: ""
|
14
|
+
const :breakdown, T::Array[T::Array[String]], default: []
|
15
|
+
|
16
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
11
17
|
def to_h
|
12
18
|
{
|
13
19
|
value: value,
|
@@ -16,6 +22,9 @@ module EN14960
|
|
16
22
|
}
|
17
23
|
end
|
18
24
|
|
19
|
-
|
25
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
26
|
+
def as_json
|
27
|
+
to_h
|
28
|
+
end
|
20
29
|
end
|
21
30
|
end
|
data/lib/en14960/source_code.rb
CHANGED
@@ -1,17 +1,29 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
# typed: strict
|
3
|
+
|
4
|
+
require "sorbet-runtime"
|
2
5
|
|
3
6
|
module EN14960
|
4
7
|
module SourceCode
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
sig { params(method_name: Symbol, module_name: Module, additional_methods: T::Array[Symbol]).returns(String) }
|
5
11
|
def self.get_method_source(method_name, module_name, additional_methods = [])
|
6
|
-
|
7
|
-
|
12
|
+
base_dir = File.expand_path("..", __FILE__)
|
13
|
+
|
14
|
+
ruby_files = Dir.glob(File.join(base_dir, "**", "*.rb"))
|
8
15
|
|
9
|
-
|
16
|
+
file_path, line_number = find_method_in_files(ruby_files, method_name)
|
10
17
|
|
11
|
-
file_path
|
12
|
-
|
18
|
+
unless file_path
|
19
|
+
raise StandardError, "Source code not available for method: #{method_name}"
|
20
|
+
end
|
21
|
+
|
22
|
+
unless File.exist?(file_path)
|
23
|
+
raise StandardError, "Source file not found: #{file_path}"
|
24
|
+
end
|
13
25
|
|
14
|
-
lines = File.readlines(file_path)
|
26
|
+
lines = File.readlines(file_path, encoding: "UTF-8")
|
15
27
|
|
16
28
|
constants_code = ""
|
17
29
|
module_constants = get_module_constants(module_name, method_name)
|
@@ -27,10 +39,9 @@ module EN14960
|
|
27
39
|
|
28
40
|
additional_methods.each do |additional_method|
|
29
41
|
if module_name.respond_to?(additional_method)
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
additional_lines = extract_method_lines(lines, additional_location[1] - 1, additional_method)
|
42
|
+
method_line_idx = lines.index { |line| line.strip =~ /^def\s+(self\.)?#{Regexp.escape(additional_method.to_s)}(\(|$|\s)/ }
|
43
|
+
if method_line_idx
|
44
|
+
additional_lines = extract_method_lines(lines, method_line_idx, additional_method)
|
34
45
|
methods_code += strip_consistent_indentation(additional_lines.join("")) + "\n\n"
|
35
46
|
end
|
36
47
|
end
|
@@ -50,12 +61,31 @@ module EN14960
|
|
50
61
|
output
|
51
62
|
end
|
52
63
|
|
64
|
+
sig { params(files: T::Array[String], method_name: Symbol).returns(T.nilable([String, Integer])) }
|
65
|
+
private_class_method def self.find_method_in_files(files, method_name)
|
66
|
+
files.each do |path|
|
67
|
+
if File.exist?(path)
|
68
|
+
content = File.read(path, encoding: "UTF-8")
|
69
|
+
if content.match?(/def\s+(self\.)?#{Regexp.escape(method_name.to_s)}(\(|\s|$)/)
|
70
|
+
lines = File.readlines(path, encoding: "UTF-8")
|
71
|
+
line_idx = lines.index { |line| line.strip =~ /^def\s+(self\.)?#{Regexp.escape(method_name.to_s)}(\(|$|\s)/ }
|
72
|
+
if line_idx
|
73
|
+
return [path, line_idx + 1]
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
nil
|
79
|
+
end
|
80
|
+
|
81
|
+
sig { params(module_name: Module, method_name: Symbol).returns(T::Array[Symbol]) }
|
53
82
|
private_class_method def self.get_module_constants(module_name, method_name)
|
54
83
|
module_name.constants.select do |const_name|
|
55
84
|
module_name.const_get(const_name).is_a?(Hash)
|
56
85
|
end
|
57
86
|
end
|
58
87
|
|
88
|
+
sig { params(lines: T::Array[String], constant_name: Symbol).returns(String) }
|
59
89
|
private_class_method def self.extract_constant_definition(lines, constant_name)
|
60
90
|
constant_lines = []
|
61
91
|
in_constant = false
|
@@ -82,6 +112,7 @@ module EN14960
|
|
82
112
|
constant_lines.join("")
|
83
113
|
end
|
84
114
|
|
115
|
+
sig { params(lines: T::Array[String], start_line: Integer, method_name: Symbol).returns(T::Array[String]) }
|
85
116
|
private_class_method def self.extract_method_lines(lines, start_line, method_name)
|
86
117
|
method_lines = []
|
87
118
|
current_line = start_line
|
@@ -89,7 +120,7 @@ module EN14960
|
|
89
120
|
|
90
121
|
while current_line < lines.length
|
91
122
|
line = lines[current_line]
|
92
|
-
if
|
123
|
+
if /^def\s+(self\.)?#{Regexp.escape(method_name.to_s)}(\(|$|\s)/.match?(line.strip)
|
93
124
|
indent_level = line.index("def")
|
94
125
|
method_lines << line
|
95
126
|
current_line += 1
|
@@ -104,7 +135,7 @@ module EN14960
|
|
104
135
|
line = lines[current_line]
|
105
136
|
method_lines << line
|
106
137
|
|
107
|
-
if line.strip == "end" && (line.index(/\S/) || 0) <= indent_level
|
138
|
+
if line.strip == "end" && indent_level && (line.index(/\S/) || 0) <= indent_level
|
108
139
|
break
|
109
140
|
end
|
110
141
|
|
@@ -114,6 +145,7 @@ module EN14960
|
|
114
145
|
method_lines
|
115
146
|
end
|
116
147
|
|
148
|
+
sig { params(source_code: String).returns(String) }
|
117
149
|
private_class_method def self.strip_consistent_indentation(source_code)
|
118
150
|
lines = source_code.split("\n")
|
119
151
|
|
@@ -1,51 +1,55 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
# typed: strict
|
2
3
|
|
4
|
+
require "sorbet-runtime"
|
3
5
|
require_relative "../constants"
|
4
6
|
|
5
7
|
module EN14960
|
6
8
|
module Validators
|
7
9
|
module MaterialValidator
|
10
|
+
extend T::Sig
|
8
11
|
extend self
|
9
12
|
|
13
|
+
sig { params(diameter_mm: Float).returns(T::Boolean) }
|
10
14
|
def valid_rope_diameter?(diameter_mm)
|
11
15
|
# EN 14960:2019 - Rope diameter range prevents finger entrapment while
|
12
16
|
# ensuring adequate grip and structural strength
|
13
|
-
return false if diameter_mm.nil?
|
14
17
|
|
15
18
|
min_diameter = Constants::MATERIAL_STANDARDS[:rope][:min_diameter]
|
16
19
|
max_diameter = Constants::MATERIAL_STANDARDS[:rope][:max_diameter]
|
17
20
|
diameter_mm.between?(min_diameter, max_diameter)
|
18
21
|
end
|
19
22
|
|
23
|
+
sig { returns(String) }
|
20
24
|
def fabric_tensile_requirement
|
21
25
|
fabric_standards = Constants::MATERIAL_STANDARDS[:fabric]
|
22
26
|
"#{fabric_standards[:min_tensile_strength]} Newtons minimum"
|
23
27
|
end
|
24
28
|
|
29
|
+
sig { returns(String) }
|
25
30
|
def fabric_tear_requirement
|
26
31
|
fabric_standards = Constants::MATERIAL_STANDARDS[:fabric]
|
27
32
|
"#{fabric_standards[:min_tear_strength]} Newtons minimum"
|
28
33
|
end
|
29
34
|
|
30
35
|
# Additional validation methods
|
36
|
+
sig { params(strength_n: Float).returns(T::Boolean) }
|
31
37
|
def valid_fabric_tensile_strength?(strength_n)
|
32
|
-
return false if strength_n.nil?
|
33
38
|
strength_n >= Constants::MATERIAL_STANDARDS[:fabric][:min_tensile_strength]
|
34
39
|
end
|
35
40
|
|
41
|
+
sig { params(strength_n: Float).returns(T::Boolean) }
|
36
42
|
def valid_fabric_tear_strength?(strength_n)
|
37
|
-
return false if strength_n.nil?
|
38
43
|
strength_n >= Constants::MATERIAL_STANDARDS[:fabric][:min_tear_strength]
|
39
44
|
end
|
40
45
|
|
46
|
+
sig { params(strength_n: Float).returns(T::Boolean) }
|
41
47
|
def valid_thread_tensile_strength?(strength_n)
|
42
|
-
return false if strength_n.nil?
|
43
48
|
strength_n >= Constants::MATERIAL_STANDARDS[:thread][:min_tensile_strength]
|
44
49
|
end
|
45
50
|
|
51
|
+
sig { params(mesh_mm: Float, is_roof: T::Boolean).returns(T::Boolean) }
|
46
52
|
def valid_netting_mesh?(mesh_mm, is_roof: false)
|
47
|
-
return false if mesh_mm.nil?
|
48
|
-
|
49
53
|
max_mesh = is_roof ?
|
50
54
|
Constants::MATERIAL_STANDARDS[:netting][:max_roof_mesh] :
|
51
55
|
Constants::MATERIAL_STANDARDS[:netting][:max_vertical_mesh]
|
@@ -1,10 +1,23 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
# typed: strict
|
3
|
+
|
4
|
+
require "sorbet-runtime"
|
2
5
|
|
3
6
|
module EN14960
|
4
7
|
module Validators
|
5
8
|
module PlayAreaValidator
|
9
|
+
extend T::Sig
|
6
10
|
extend self
|
7
11
|
|
12
|
+
sig {
|
13
|
+
params(
|
14
|
+
unit_length: Float,
|
15
|
+
unit_width: Float,
|
16
|
+
play_area_length: Float,
|
17
|
+
play_area_width: Float,
|
18
|
+
negative_adjustment_area: Float
|
19
|
+
).returns(T::Hash[Symbol, T.untyped])
|
20
|
+
}
|
8
21
|
def validate(
|
9
22
|
unit_length:,
|
10
23
|
unit_width:,
|
@@ -14,17 +27,7 @@ module EN14960
|
|
14
27
|
)
|
15
28
|
errors = []
|
16
29
|
|
17
|
-
|
18
|
-
unit_length,
|
19
|
-
unit_width,
|
20
|
-
play_area_length,
|
21
|
-
play_area_width,
|
22
|
-
negative_adjustment_area
|
23
|
-
].any?(&:nil?)
|
24
|
-
errors << "All measurements must be provided"
|
25
|
-
end
|
26
|
-
|
27
|
-
return build_response(false, errors) unless errors.empty?
|
30
|
+
# All parameters are required, no nil checks needed
|
28
31
|
|
29
32
|
unit_length = unit_length.to_f
|
30
33
|
unit_width = unit_width.to_f
|
@@ -61,6 +64,7 @@ module EN14960
|
|
61
64
|
|
62
65
|
private
|
63
66
|
|
67
|
+
sig { params(valid: T::Boolean, errors: T::Array[String], measurements: T::Hash[Symbol, Float]).returns(T::Hash[Symbol, T.untyped]) }
|
64
68
|
def build_response(valid, errors, measurements = {})
|
65
69
|
{
|
66
70
|
valid: valid,
|
data/lib/en14960/version.rb
CHANGED
data/lib/en14960.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
# typed: strict
|
2
3
|
|
4
|
+
require "sorbet-runtime"
|
3
5
|
require_relative "en14960/version"
|
4
6
|
require_relative "en14960/models/calculator_response"
|
5
7
|
require_relative "en14960/constants"
|
@@ -13,15 +15,19 @@ require_relative "en14960/source_code"
|
|
13
15
|
# EN14960 provides calculators and validators for BS EN 14960:2019
|
14
16
|
# the safety standard for inflatable play equipment
|
15
17
|
module EN14960
|
18
|
+
extend T::Sig
|
19
|
+
|
16
20
|
class Error < StandardError; end
|
17
21
|
|
18
22
|
# Public API methods for easy access to calculators
|
19
23
|
class << self
|
24
|
+
extend T::Sig
|
20
25
|
# Calculate required anchors for inflatable play equipment
|
21
26
|
# @param length [Float] Length in meters
|
22
27
|
# @param width [Float] Width in meters
|
23
28
|
# @param height [Float] Height in meters
|
24
29
|
# @return [CalculatorResponse] Response with anchor count and breakdown
|
30
|
+
sig { params(length: Float, width: Float, height: Float).returns(CalculatorResponse) }
|
25
31
|
def calculate_anchors(length:, width:, height:)
|
26
32
|
Calculators::AnchorCalculator.calculate(length: length, width: width, height: height)
|
27
33
|
end
|
@@ -30,6 +36,7 @@ module EN14960
|
|
30
36
|
# @param platform_height [Float] Platform height in meters
|
31
37
|
# @param has_stop_wall [Boolean] Whether a stop wall is fitted
|
32
38
|
# @return [CalculatorResponse] Response with runout distance and breakdown
|
39
|
+
sig { params(platform_height: Float, has_stop_wall: T::Boolean).returns(CalculatorResponse) }
|
33
40
|
def calculate_slide_runout(platform_height, has_stop_wall: false)
|
34
41
|
Calculators::SlideCalculator.calculate_required_runout(platform_height, has_stop_wall: has_stop_wall)
|
35
42
|
end
|
@@ -39,6 +46,7 @@ module EN14960
|
|
39
46
|
# @param user_height [Float] Maximum user height in meters
|
40
47
|
# @param has_permanent_roof [Boolean] Whether unit has permanent roof
|
41
48
|
# @return [CalculatorResponse] Response with wall height requirements
|
49
|
+
sig { params(platform_height: Float, user_height: Float, has_permanent_roof: T.nilable(T::Boolean)).returns(CalculatorResponse) }
|
42
50
|
def calculate_wall_height(platform_height, user_height, has_permanent_roof = nil)
|
43
51
|
Calculators::SlideCalculator.calculate_wall_height_requirements(
|
44
52
|
platform_height,
|
@@ -53,7 +61,8 @@ module EN14960
|
|
53
61
|
# @param max_user_height [Float, nil] Maximum allowed user height
|
54
62
|
# @param negative_adjustment_area [Float] Area to subtract for obstacles
|
55
63
|
# @return [CalculatorResponse] Response with capacity by user height
|
56
|
-
|
64
|
+
sig { params(length: Float, width: Float, max_user_height: T.nilable(Float), negative_adjustment_area: Float).returns(CalculatorResponse) }
|
65
|
+
def calculate_user_capacity(length, width, max_user_height = nil, negative_adjustment_area = 0.0)
|
57
66
|
Calculators::UserCapacityCalculator.calculate(
|
58
67
|
length,
|
59
68
|
width,
|
@@ -65,18 +74,21 @@ module EN14960
|
|
65
74
|
# Check if rope diameter meets safety requirements
|
66
75
|
# @param diameter_mm [Float] Rope diameter in millimeters
|
67
76
|
# @return [Boolean] Whether diameter is within safe range
|
77
|
+
sig { params(diameter_mm: Float).returns(T::Boolean) }
|
68
78
|
def valid_rope_diameter?(diameter_mm)
|
69
79
|
Validators::MaterialValidator.valid_rope_diameter?(diameter_mm)
|
70
80
|
end
|
71
81
|
|
72
82
|
# Get height categories defined by EN 14960:2019
|
73
83
|
# @return [Hash] Height categories with labels and requirements
|
84
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
74
85
|
def height_categories
|
75
86
|
Constants::HEIGHT_CATEGORIES
|
76
87
|
end
|
77
88
|
|
78
89
|
# Get material standards defined by EN 14960:2019
|
79
90
|
# @return [Hash] Material requirements for fabrics, threads, ropes, and netting
|
91
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
80
92
|
def material_standards
|
81
93
|
Constants::MATERIAL_STANDARDS
|
82
94
|
end
|
@@ -88,6 +100,15 @@ module EN14960
|
|
88
100
|
# @param play_area_width [Float] Play area width
|
89
101
|
# @param negative_adjustment_area [Float] Negative adjustment area
|
90
102
|
# @return [Hash] Validation result with errors and measurements
|
103
|
+
sig {
|
104
|
+
params(
|
105
|
+
unit_length: Float,
|
106
|
+
unit_width: Float,
|
107
|
+
play_area_length: Float,
|
108
|
+
play_area_width: Float,
|
109
|
+
negative_adjustment_area: Float
|
110
|
+
).returns(T::Hash[Symbol, T.untyped])
|
111
|
+
}
|
91
112
|
def validate_play_area(
|
92
113
|
unit_length:,
|
93
114
|
unit_width:,
|
data/sorbet/config
ADDED
data/terragon-setup.sh
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
#\!/bin/bash
|
2
|
+
set -euo pipefail
|
3
|
+
|
4
|
+
# Terragon Labs Rails Setup Script
|
5
|
+
# Optimized for Ubuntu 24.04 - runs on every sandbox start
|
6
|
+
|
7
|
+
# Skip if already set up (optimization for repeated runs)
|
8
|
+
if [ -f ".terragon-setup-complete" ] && [ -z "${FORCE_SETUP:-}" ]; then
|
9
|
+
echo "Setup already complete. Run with FORCE_SETUP=1 to re-run."
|
10
|
+
exit 0
|
11
|
+
fi
|
12
|
+
|
13
|
+
echo "Installing Ruby and dependencies..."
|
14
|
+
sudo apt-get update -qq
|
15
|
+
sudo apt-get install -y ruby-full ruby-bundler build-essential libsqlite3-dev libyaml-dev imagemagick
|
16
|
+
|
17
|
+
echo "Installing bundler gem..."
|
18
|
+
sudo gem install bundler
|
19
|
+
|
20
|
+
# Setup ImageMagick symlinks if needed
|
21
|
+
for cmd in identify mogrify convert; do
|
22
|
+
if \! command -v $cmd &> /dev/null; then
|
23
|
+
BINARY=$(ls /usr/bin/${cmd}-* 2>/dev/null | grep -E "${cmd}-im[0-9]" | head -1)
|
24
|
+
if [ -n "$BINARY" ]; then
|
25
|
+
sudo ln -sf "$BINARY" "/usr/local/bin/$cmd" 2>/dev/null || true
|
26
|
+
fi
|
27
|
+
fi
|
28
|
+
done
|
29
|
+
|
30
|
+
# Mark setup as complete
|
31
|
+
touch .terragon-setup-complete
|
32
|
+
|
33
|
+
echo "Rails setup complete\!"
|
34
|
+
|
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: en14960
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chobble.com
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-08-
|
11
|
+
date: 2025-08-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: sorbet-runtime
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.5'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.5'
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: bundler
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -80,6 +94,34 @@ dependencies:
|
|
80
94
|
- - "~>"
|
81
95
|
- !ruby/object:Gem::Version
|
82
96
|
version: '0.21'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: sorbet
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0.5'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0.5'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: tapioca
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0.16'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0.16'
|
83
125
|
description: A Ruby gem providing calculators and validators for BS EN 14960:2019
|
84
126
|
- the safety standard for inflatable play equipment. Includes calculations for anchoring
|
85
127
|
requirements, slide safety, user capacity, and material specifications.
|
@@ -91,6 +133,7 @@ extra_rdoc_files: []
|
|
91
133
|
files:
|
92
134
|
- ".envrc"
|
93
135
|
- ".rspec"
|
136
|
+
- ".terragon-setup-complete"
|
94
137
|
- CHANGELOG.md
|
95
138
|
- LICENSE
|
96
139
|
- LICENSE.txt
|
@@ -99,6 +142,7 @@ files:
|
|
99
142
|
- flake.lock
|
100
143
|
- flake.nix
|
101
144
|
- lib/en14960.rb
|
145
|
+
- lib/en14960/api.rb
|
102
146
|
- lib/en14960/calculators/anchor_calculator.rb
|
103
147
|
- lib/en14960/calculators/slide_calculator.rb
|
104
148
|
- lib/en14960/calculators/user_capacity_calculator.rb
|
@@ -108,6 +152,8 @@ files:
|
|
108
152
|
- lib/en14960/validators/material_validator.rb
|
109
153
|
- lib/en14960/validators/play_area_validator.rb
|
110
154
|
- lib/en14960/version.rb
|
155
|
+
- sorbet/config
|
156
|
+
- terragon-setup.sh
|
111
157
|
homepage: https://github.com/chobbledotcom/en14960
|
112
158
|
licenses:
|
113
159
|
- AGPL-3.0-or-later
|