specproof 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +10 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +283 -0
  5. data/docs/agent-skills.md +60 -0
  6. data/docs/benchmark.md +24 -0
  7. data/docs/constrained-partition-pairs-results.md +44 -0
  8. data/docs/finite-strings-results.md +75 -0
  9. data/docs/implementation-status.md +65 -0
  10. data/docs/input-selection-results.md +123 -0
  11. data/docs/lint-cleanup-plan.md +12 -0
  12. data/docs/method-targets-results.md +68 -0
  13. data/docs/minitest-compatibility.md +33 -0
  14. data/docs/optional-arguments-plan.md +25 -0
  15. data/docs/optional-arguments-results.md +45 -0
  16. data/docs/pairwise-partitions-results.md +53 -0
  17. data/docs/pairwise-results.md +77 -0
  18. data/docs/pairwise-suggestions-results.md +104 -0
  19. data/docs/rbs-implementation-status.md +115 -0
  20. data/docs/rbs-string-literals-results.md +58 -0
  21. data/docs/real-project-pilot.md +84 -0
  22. data/docs/report-format.md +244 -0
  23. data/examples/age_policy/README.md +39 -0
  24. data/examples/age_policy/faulty/lib/age_policy.rb +9 -0
  25. data/examples/age_policy/faulty/test/age_policy_examples_test.rb +17 -0
  26. data/examples/age_policy/faulty/test/age_policy_test.rb +21 -0
  27. data/examples/age_policy/lib/age_policy.rb +8 -0
  28. data/examples/age_policy/run.rb +23 -0
  29. data/examples/age_policy/specification.rb +8 -0
  30. data/examples/age_policy/test/age_policy_test.rb +18 -0
  31. data/examples/constrained_partition_pairs/README.md +40 -0
  32. data/examples/constrained_partition_pairs/lib/account_eligibility.rb +8 -0
  33. data/examples/constrained_partition_pairs/requirements.md +11 -0
  34. data/examples/constrained_partition_pairs/run.rb +22 -0
  35. data/examples/constrained_partition_pairs/specification.rb +17 -0
  36. data/examples/constrained_partition_pairs/test_account_eligibility.rb +34 -0
  37. data/examples/input_selection/README.md +22 -0
  38. data/examples/input_selection/delivery_policy.rb +11 -0
  39. data/examples/input_selection/requirements.md +9 -0
  40. data/examples/input_selection/specification.rb +8 -0
  41. data/examples/input_selection/test_delivery_policy.rb +38 -0
  42. data/examples/method_targets/README.md +30 -0
  43. data/examples/method_targets/lib/delivery_defaults.rb +8 -0
  44. data/examples/method_targets/lib/delivery_options.rb +12 -0
  45. data/examples/method_targets/requirements.md +20 -0
  46. data/examples/method_targets/specification_objects.rb +17 -0
  47. data/examples/method_targets/specification_strings.rb +15 -0
  48. data/examples/pairwise/README.md +46 -0
  49. data/examples/pairwise/constrained_requirements.md +6 -0
  50. data/examples/pairwise/constrained_specification.rb +11 -0
  51. data/examples/pairwise/corrected/lib/delivery_options.rb +11 -0
  52. data/examples/pairwise/corrected/test/delivery_options_test.rb +28 -0
  53. data/examples/pairwise/faulty/lib/delivery_options.rb +11 -0
  54. data/examples/pairwise/faulty/test/gap_delivery_options_test.rb +10 -0
  55. data/examples/pairwise/faulty/test/initial_delivery_options_test.rb +23 -0
  56. data/examples/pairwise/requirements.md +11 -0
  57. data/examples/pairwise/run.rb +36 -0
  58. data/examples/pairwise/specification.rb +9 -0
  59. data/examples/pairwise/three_boolean/lib/boolean_batch.rb +8 -0
  60. data/examples/pairwise/three_boolean/test/boolean_batch_test.rb +28 -0
  61. data/examples/pairwise/three_boolean_specification.rb +10 -0
  62. data/examples/pairwise_partitions/README.md +34 -0
  63. data/examples/pairwise_partitions/lib/age_policy.rb +8 -0
  64. data/examples/pairwise_partitions/requirements.md +8 -0
  65. data/examples/pairwise_partitions/run.rb +22 -0
  66. data/examples/pairwise_partitions/specification.rb +13 -0
  67. data/examples/pairwise_partitions/test_age_policy.rb +29 -0
  68. data/examples/pairwise_suggestions/README.md +48 -0
  69. data/examples/pairwise_suggestions/run.rb +55 -0
  70. data/examples/pairwise_suggestions/three_boolean/lib/suggested_boolean_batch.rb +8 -0
  71. data/examples/pairwise_suggestions/three_boolean/requirements.md +7 -0
  72. data/examples/pairwise_suggestions/three_boolean/specification.rb +11 -0
  73. data/examples/pairwise_suggestions/three_boolean/test/baseline_test.rb +9 -0
  74. data/examples/pairwise_suggestions/three_boolean/test/complete_test.rb +22 -0
  75. data/examples/partition_gap.rb +8 -0
  76. data/examples/rbs_registration/README.md +58 -0
  77. data/examples/rbs_registration/implementations/age_gt/registration.rb +8 -0
  78. data/examples/rbs_registration/implementations/age_gte/registration.rb +8 -0
  79. data/examples/rbs_registration/requirements.md +9 -0
  80. data/examples/rbs_registration/sig/registration.rbs +3 -0
  81. data/examples/rbs_registration/specification.rb +10 -0
  82. data/examples/rbs_registration/test_registration.rb +43 -0
  83. data/examples/rbs_string_literals/README.md +34 -0
  84. data/examples/rbs_string_literals/lib/delivery_options.rb +9 -0
  85. data/examples/rbs_string_literals/requirements.md +11 -0
  86. data/examples/rbs_string_literals/sig/delivery_options.rbs +3 -0
  87. data/examples/rbs_string_literals/specification.rb +7 -0
  88. data/examples/rbs_string_literals/test_delivery_options.rb +46 -0
  89. data/examples/shipping.rb +12 -0
  90. data/exe/specproof +7 -0
  91. data/lib/specproof/checker.rb +144 -0
  92. data/lib/specproof/cli.rb +292 -0
  93. data/lib/specproof/collector.rb +304 -0
  94. data/lib/specproof/combinations.rb +226 -0
  95. data/lib/specproof/composer.rb +97 -0
  96. data/lib/specproof/coverage.rb +229 -0
  97. data/lib/specproof/coverage_report.rb +334 -0
  98. data/lib/specproof/domains.rb +124 -0
  99. data/lib/specproof/loader.rb +285 -0
  100. data/lib/specproof/minitest.rb +443 -0
  101. data/lib/specproof/model.rb +198 -0
  102. data/lib/specproof/partition_space.rb +268 -0
  103. data/lib/specproof/planner.rb +125 -0
  104. data/lib/specproof/report.rb +248 -0
  105. data/lib/specproof/skills.rb +188 -0
  106. data/lib/specproof/sources/rbs.rb +137 -0
  107. data/lib/specproof/sources/rbs_projector.rb +268 -0
  108. data/lib/specproof/suggestion_report.rb +159 -0
  109. data/lib/specproof/suggestions.rb +352 -0
  110. data/lib/specproof/target_name.rb +100 -0
  111. data/lib/specproof/type_constraints.rb +76 -0
  112. data/lib/specproof/version.rb +7 -0
  113. data/lib/specproof.rb +100 -0
  114. data/sig/specproof.rbs +222 -0
  115. data/skills/specproof-specify/SKILL.md +282 -0
  116. data/skills/specproof-specify/references/usage.md +265 -0
  117. data/skills/specproof-test-gaps/SKILL.md +214 -0
  118. data/skills/specproof-test-gaps/references/usage.md +231 -0
  119. metadata +165 -0
@@ -0,0 +1,285 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Loads specification files without resolving application constants.
4
+ module SpecProof
5
+ # Builds and validates declarations while a specification file runs.
6
+ module Loader
7
+ # Collects the inputs declared for one target.
8
+ class Builder
9
+ attr_reader :target, :source, :inputs, :exclusions, :combination_group
10
+
11
+ def initialize(target, source)
12
+ @target = target
13
+ @source = source
14
+ @inputs = {}
15
+ @exclusions = {}
16
+ @combination_group = nil
17
+ end
18
+
19
+ def input(name, **options, &block)
20
+ location = Loader.call_source
21
+ name = valid_name(name, location, "input")
22
+ duplicate!(inputs, name, "duplicate_input", location)
23
+ overlap!(name, location)
24
+ domain, form = parse_domain(options, location)
25
+ exhaustive = partition_policy(options, :exhaustive, :allow_gaps, location)
26
+ disjoint = partition_policy(options, :disjoint, :allow_overlaps, location)
27
+ unknown = options.keys - %i[range values exhaustive disjoint allow_gaps allow_overlaps]
28
+ unknown_option!(unknown.first, location) unless unknown.empty?
29
+ partitions = []
30
+ if block
31
+ partition_builder = PartitionBuilder.new(domain, form)
32
+ partition_builder.instance_eval(&block)
33
+ partitions = partition_builder.partitions
34
+ end
35
+ if (exhaustive || disjoint) && partitions.empty?
36
+ raise ConfigurationError.new("a promised partition family must not be empty", code: "empty_partition_family",
37
+ source: location)
38
+ end
39
+
40
+ inputs[name] = Input.new(name: name, domain: domain, partitions: partitions,
41
+ exhaustive: exhaustive, disjoint: disjoint, source: location)
42
+ end
43
+
44
+ def exclude_input(name, reason:)
45
+ location = Loader.call_source
46
+ name = valid_name(name, location, "input")
47
+ duplicate!(exclusions, name, "duplicate_exclusion", location)
48
+ overlap!(name, location)
49
+ unless reason.is_a?(String) && !reason.strip.empty?
50
+ raise ConfigurationError.new("exclusion reason must be a non-blank String",
51
+ code: "invalid_exclusion_reason", source: location)
52
+ end
53
+
54
+ exclusions[name] = Exclusion.new(name: name, reason: reason, source: location)
55
+ end
56
+
57
+ def combinations(*names, **options, &block)
58
+ location = Loader.call_source
59
+ if combination_group
60
+ raise ConfigurationError.new("a target may declare only one combinations group",
61
+ code: "duplicate_combinations", source: location)
62
+ end
63
+ unless names.length >= 2 && names.all? { |name| name.is_a?(Symbol) && !name.to_s.empty? } && names.uniq == names
64
+ raise ConfigurationError.new("combinations requires at least two unique non-empty input symbols",
65
+ code: "invalid_combination_inputs", source: location)
66
+ end
67
+ unknown = options.keys - [:all_allowed]
68
+ unknown_option!(unknown.first, location) unless unknown.empty?
69
+
70
+ forbidden = if block
71
+ unless options.empty?
72
+ raise ConfigurationError.new("forbid block cannot be combined with options",
73
+ code: "invalid_combinations_mode", source: location)
74
+ end
75
+ builder = CombinationBuilder.new(names, location)
76
+ builder.instance_eval(&block)
77
+ if builder.forbidden.empty?
78
+ raise ConfigurationError.new("combinations block must declare at least one forbid",
79
+ code: "empty_combinations_block", source: location)
80
+ end
81
+ builder.forbidden
82
+ else
83
+ unless options.keys == [:all_allowed] && options[:all_allowed].equal?(true)
84
+ raise ConfigurationError.new("declare all_allowed: true or provide a non-empty forbid block",
85
+ code: "invalid_combinations_mode", source: location)
86
+ end
87
+ []
88
+ end
89
+ @combination_group = CombinationGroup.new(inputs: names, forbidden: forbidden, source: location)
90
+ end
91
+
92
+ private
93
+
94
+ def parse_domain(options, source)
95
+ has_range = options.key?(:range)
96
+ has_values = options.key?(:values)
97
+ if has_range == has_values
98
+ raise ConfigurationError.new("declare exactly one of range: or values:", code: "domain_required",
99
+ source: source)
100
+ end
101
+
102
+ if has_range
103
+ [Domains.from_range(options[:range], source: source),
104
+ :range]
105
+ else
106
+ [Domains.from_values(options[:values], source: source), :values]
107
+ end
108
+ end
109
+
110
+ def flag(options, key, source)
111
+ value = options.fetch(key, false)
112
+ return value if value.equal?(true) || value.equal?(false)
113
+
114
+ raise ConfigurationError.new("#{key} must be true or false", code: "invalid_flag", source: source)
115
+ end
116
+
117
+ def partition_policy(options, legacy_key, readable_key, source)
118
+ if options.key?(legacy_key) && options.key?(readable_key)
119
+ raise ConfigurationError.new("declare only one of #{legacy_key}: or #{readable_key}:",
120
+ code: "duplicate_partition_policy", source: source)
121
+ end
122
+
123
+ return flag(options, legacy_key, source) if options.key?(legacy_key)
124
+ return !flag(options, readable_key, source) if options.key?(readable_key)
125
+
126
+ false
127
+ end
128
+
129
+ def valid_name(name, source, type)
130
+ unless name.is_a?(Symbol) && !name.to_s.empty?
131
+ raise ConfigurationError.new("#{type} name must be a non-empty symbol", code: "invalid_name", source: source)
132
+ end
133
+
134
+ name
135
+ end
136
+
137
+ def duplicate!(collection, name, code, source)
138
+ return unless collection.key?(name)
139
+
140
+ raise ConfigurationError.new("duplicate declaration for #{name.inspect}", code: code, source: source)
141
+ end
142
+
143
+ def overlap!(name, source)
144
+ return unless inputs.key?(name) || exclusions.key?(name)
145
+
146
+ raise ConfigurationError.new("input #{name.inspect} cannot be both analyzed and excluded",
147
+ code: "input_exclusion_overlap", source: source)
148
+ end
149
+
150
+ def unknown_option!(option, source)
151
+ raise ConfigurationError.new("unknown declaration option #{option.inspect}", code: "unknown_option",
152
+ source: source)
153
+ end
154
+ end
155
+
156
+ # Collects partitions for one input declaration.
157
+ class PartitionBuilder
158
+ attr_reader :partitions
159
+
160
+ def initialize(parent_domain, parent_form)
161
+ @parent_domain = parent_domain
162
+ @parent_form = parent_form
163
+ @partitions = []
164
+ @names = {}
165
+ end
166
+
167
+ def partition(name, **options)
168
+ source = Loader.call_source
169
+ unless name.is_a?(Symbol) && !name.to_s.empty?
170
+ raise ConfigurationError.new("partition name must be a non-empty symbol", code: "invalid_name",
171
+ source: source)
172
+ end
173
+ if @names.key?(name)
174
+ raise ConfigurationError.new("duplicate partition #{name.inspect}", code: "duplicate_partition",
175
+ source: source)
176
+ end
177
+
178
+ domain_keys = options.keys & %i[range values]
179
+ unless domain_keys.length == 1 && options.keys.length == 1
180
+ raise ConfigurationError.new("partition must declare exactly one of range: or values:",
181
+ code: "invalid_partition_domain", source: source)
182
+ end
183
+
184
+ form = domain_keys.first == :range ? :range : :values
185
+ unless form == @parent_form
186
+ raise ConfigurationError.new("partition domain form must match its input", code: "partition_domain_kind",
187
+ source: source)
188
+ end
189
+
190
+ domain = if form == :range
191
+ Domains.from_range(options[:range],
192
+ source: source)
193
+ else
194
+ Domains.from_values(options[:values],
195
+ source: source)
196
+ end
197
+ @names[name] = true
198
+ partitions << Partition.new(name: name, domain: domain, source: source)
199
+ end
200
+ end
201
+
202
+ class << self
203
+ def load(paths, allow_exclusion_only: false)
204
+ paths = Array(paths)
205
+ raise ConfigurationError.new("provide at least one specification file", code: "paths_required") if paths.empty?
206
+ raise ConfigurationError.new("nested specification loading is not supported", code: "nested_load") if @active
207
+
208
+ declarations = {}
209
+ owns_context = false
210
+ @active = declarations
211
+ @allow_exclusion_only = allow_exclusion_only
212
+ owns_context = true
213
+ paths.map { |path| File.expand_path(File.path(path)) }.sort.each do |path|
214
+ unless File.file?(path)
215
+ raise ConfigurationError.new("specification file does not exist: #{path}", code: "file_not_found",
216
+ path: path)
217
+ end
218
+
219
+ @current_path = path
220
+ TOPLEVEL_BINDING.eval(File.read(path), path, 1)
221
+ end
222
+ if declarations.empty?
223
+ raise ConfigurationError.new("no specifications were declared", code: "empty_specifications")
224
+ end
225
+
226
+ unless allow_exclusion_only
227
+ declarations.each_value do |specification|
228
+ next unless specification.combination_group
229
+
230
+ PartitionSpace.for_specification(specification, validate_geometry: false)
231
+ end
232
+ end
233
+
234
+ declarations.values.freeze
235
+ rescue Errno::ENOENT, Errno::EACCES => e
236
+ raise ConfigurationError.new(e.message, code: "file_error",
237
+ path: e.respond_to?(:path) ? e.path : nil)
238
+ ensure
239
+ if owns_context
240
+ @active = nil
241
+ @allow_exclusion_only = nil
242
+ @current_path = nil
243
+ end
244
+ end
245
+
246
+ def define(target, &block)
247
+ source = call_source
248
+ unless @active
249
+ raise ConfigurationError.new("SpecProof.define may only be called while loading a specification",
250
+ code: "outside_load", source: source)
251
+ end
252
+ target = TargetName.normalize(target, source: source)
253
+ if @active.key?(target)
254
+ raise ConfigurationError.new("duplicate target #{target}", code: "duplicate_target", source: source)
255
+ end
256
+ unless block
257
+ raise ConfigurationError.new("target declaration requires a block", code: "missing_block", source: source)
258
+ end
259
+
260
+ builder = Builder.new(target, source)
261
+ builder.instance_eval(&block)
262
+ allowed_empty = @allow_exclusion_only && (!builder.exclusions.empty? || builder.combination_group)
263
+ if builder.inputs.empty? && !allowed_empty
264
+ raise ConfigurationError.new("target must declare at least one input", code: "inputs_required",
265
+ source: source)
266
+ end
267
+
268
+ @active[target] = Specification.new(target: target, inputs: builder.inputs, exclusions: builder.exclusions,
269
+ combination_group: builder.combination_group, source: source)
270
+ end
271
+
272
+ def call_source
273
+ location = caller_locations.find do |entry|
274
+ !entry.path.end_with?("/specproof/loader.rb") && !entry.path.end_with?("/specproof/combinations.rb")
275
+ end
276
+ path = location&.absolute_path || location&.path || @current_path || "(unknown)"
277
+ Source.new(path: path, line: location&.lineno || 1)
278
+ end
279
+ end
280
+ end
281
+
282
+ def self.define(target, &)
283
+ Loader.define(target, &)
284
+ end
285
+ end