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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +283 -0
- data/docs/agent-skills.md +60 -0
- data/docs/benchmark.md +24 -0
- data/docs/constrained-partition-pairs-results.md +44 -0
- data/docs/finite-strings-results.md +75 -0
- data/docs/implementation-status.md +65 -0
- data/docs/input-selection-results.md +123 -0
- data/docs/lint-cleanup-plan.md +12 -0
- data/docs/method-targets-results.md +68 -0
- data/docs/minitest-compatibility.md +33 -0
- data/docs/optional-arguments-plan.md +25 -0
- data/docs/optional-arguments-results.md +45 -0
- data/docs/pairwise-partitions-results.md +53 -0
- data/docs/pairwise-results.md +77 -0
- data/docs/pairwise-suggestions-results.md +104 -0
- data/docs/rbs-implementation-status.md +115 -0
- data/docs/rbs-string-literals-results.md +58 -0
- data/docs/real-project-pilot.md +84 -0
- data/docs/report-format.md +244 -0
- data/examples/age_policy/README.md +39 -0
- data/examples/age_policy/faulty/lib/age_policy.rb +9 -0
- data/examples/age_policy/faulty/test/age_policy_examples_test.rb +17 -0
- data/examples/age_policy/faulty/test/age_policy_test.rb +21 -0
- data/examples/age_policy/lib/age_policy.rb +8 -0
- data/examples/age_policy/run.rb +23 -0
- data/examples/age_policy/specification.rb +8 -0
- data/examples/age_policy/test/age_policy_test.rb +18 -0
- data/examples/constrained_partition_pairs/README.md +40 -0
- data/examples/constrained_partition_pairs/lib/account_eligibility.rb +8 -0
- data/examples/constrained_partition_pairs/requirements.md +11 -0
- data/examples/constrained_partition_pairs/run.rb +22 -0
- data/examples/constrained_partition_pairs/specification.rb +17 -0
- data/examples/constrained_partition_pairs/test_account_eligibility.rb +34 -0
- data/examples/input_selection/README.md +22 -0
- data/examples/input_selection/delivery_policy.rb +11 -0
- data/examples/input_selection/requirements.md +9 -0
- data/examples/input_selection/specification.rb +8 -0
- data/examples/input_selection/test_delivery_policy.rb +38 -0
- data/examples/method_targets/README.md +30 -0
- data/examples/method_targets/lib/delivery_defaults.rb +8 -0
- data/examples/method_targets/lib/delivery_options.rb +12 -0
- data/examples/method_targets/requirements.md +20 -0
- data/examples/method_targets/specification_objects.rb +17 -0
- data/examples/method_targets/specification_strings.rb +15 -0
- data/examples/pairwise/README.md +46 -0
- data/examples/pairwise/constrained_requirements.md +6 -0
- data/examples/pairwise/constrained_specification.rb +11 -0
- data/examples/pairwise/corrected/lib/delivery_options.rb +11 -0
- data/examples/pairwise/corrected/test/delivery_options_test.rb +28 -0
- data/examples/pairwise/faulty/lib/delivery_options.rb +11 -0
- data/examples/pairwise/faulty/test/gap_delivery_options_test.rb +10 -0
- data/examples/pairwise/faulty/test/initial_delivery_options_test.rb +23 -0
- data/examples/pairwise/requirements.md +11 -0
- data/examples/pairwise/run.rb +36 -0
- data/examples/pairwise/specification.rb +9 -0
- data/examples/pairwise/three_boolean/lib/boolean_batch.rb +8 -0
- data/examples/pairwise/three_boolean/test/boolean_batch_test.rb +28 -0
- data/examples/pairwise/three_boolean_specification.rb +10 -0
- data/examples/pairwise_partitions/README.md +34 -0
- data/examples/pairwise_partitions/lib/age_policy.rb +8 -0
- data/examples/pairwise_partitions/requirements.md +8 -0
- data/examples/pairwise_partitions/run.rb +22 -0
- data/examples/pairwise_partitions/specification.rb +13 -0
- data/examples/pairwise_partitions/test_age_policy.rb +29 -0
- data/examples/pairwise_suggestions/README.md +48 -0
- data/examples/pairwise_suggestions/run.rb +55 -0
- data/examples/pairwise_suggestions/three_boolean/lib/suggested_boolean_batch.rb +8 -0
- data/examples/pairwise_suggestions/three_boolean/requirements.md +7 -0
- data/examples/pairwise_suggestions/three_boolean/specification.rb +11 -0
- data/examples/pairwise_suggestions/three_boolean/test/baseline_test.rb +9 -0
- data/examples/pairwise_suggestions/three_boolean/test/complete_test.rb +22 -0
- data/examples/partition_gap.rb +8 -0
- data/examples/rbs_registration/README.md +58 -0
- data/examples/rbs_registration/implementations/age_gt/registration.rb +8 -0
- data/examples/rbs_registration/implementations/age_gte/registration.rb +8 -0
- data/examples/rbs_registration/requirements.md +9 -0
- data/examples/rbs_registration/sig/registration.rbs +3 -0
- data/examples/rbs_registration/specification.rb +10 -0
- data/examples/rbs_registration/test_registration.rb +43 -0
- data/examples/rbs_string_literals/README.md +34 -0
- data/examples/rbs_string_literals/lib/delivery_options.rb +9 -0
- data/examples/rbs_string_literals/requirements.md +11 -0
- data/examples/rbs_string_literals/sig/delivery_options.rbs +3 -0
- data/examples/rbs_string_literals/specification.rb +7 -0
- data/examples/rbs_string_literals/test_delivery_options.rb +46 -0
- data/examples/shipping.rb +12 -0
- data/exe/specproof +7 -0
- data/lib/specproof/checker.rb +144 -0
- data/lib/specproof/cli.rb +292 -0
- data/lib/specproof/collector.rb +304 -0
- data/lib/specproof/combinations.rb +226 -0
- data/lib/specproof/composer.rb +97 -0
- data/lib/specproof/coverage.rb +229 -0
- data/lib/specproof/coverage_report.rb +334 -0
- data/lib/specproof/domains.rb +124 -0
- data/lib/specproof/loader.rb +285 -0
- data/lib/specproof/minitest.rb +443 -0
- data/lib/specproof/model.rb +198 -0
- data/lib/specproof/partition_space.rb +268 -0
- data/lib/specproof/planner.rb +125 -0
- data/lib/specproof/report.rb +248 -0
- data/lib/specproof/skills.rb +188 -0
- data/lib/specproof/sources/rbs.rb +137 -0
- data/lib/specproof/sources/rbs_projector.rb +268 -0
- data/lib/specproof/suggestion_report.rb +159 -0
- data/lib/specproof/suggestions.rb +352 -0
- data/lib/specproof/target_name.rb +100 -0
- data/lib/specproof/type_constraints.rb +76 -0
- data/lib/specproof/version.rb +7 -0
- data/lib/specproof.rb +100 -0
- data/sig/specproof.rbs +222 -0
- data/skills/specproof-specify/SKILL.md +282 -0
- data/skills/specproof-specify/references/usage.md +265 -0
- data/skills/specproof-test-gaps/SKILL.md +214 -0
- data/skills/specproof-test-gaps/references/usage.md +231 -0
- metadata +165 -0
data/lib/specproof.rb
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "specproof/version"
|
|
4
|
+
require_relative "specproof/model"
|
|
5
|
+
require_relative "specproof/domains"
|
|
6
|
+
require_relative "specproof/type_constraints"
|
|
7
|
+
require_relative "specproof/combinations"
|
|
8
|
+
require_relative "specproof/partition_space"
|
|
9
|
+
require_relative "specproof/target_name"
|
|
10
|
+
require_relative "specproof/loader"
|
|
11
|
+
require_relative "specproof/composer"
|
|
12
|
+
require_relative "specproof/checker"
|
|
13
|
+
require_relative "specproof/planner"
|
|
14
|
+
require_relative "specproof/report"
|
|
15
|
+
require_relative "specproof/coverage_report"
|
|
16
|
+
require_relative "specproof/suggestions"
|
|
17
|
+
|
|
18
|
+
# Checks Ruby input-domain specifications and plans deterministic candidates.
|
|
19
|
+
module SpecProof
|
|
20
|
+
class Error < StandardError; end
|
|
21
|
+
|
|
22
|
+
# Reports invalid options and declarations with stable machine-readable codes.
|
|
23
|
+
class ConfigurationError < Error
|
|
24
|
+
attr_reader :code, :source, :path
|
|
25
|
+
|
|
26
|
+
def initialize(message, code: "configuration_error", source: nil, path: nil)
|
|
27
|
+
super(message)
|
|
28
|
+
@code = code.to_s.freeze
|
|
29
|
+
@source = source
|
|
30
|
+
@path = path&.to_s&.freeze
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Reports contradictions found in otherwise well-formed specifications.
|
|
35
|
+
class InvalidSpecificationError < Error
|
|
36
|
+
attr_reader :findings
|
|
37
|
+
|
|
38
|
+
def initialize(findings)
|
|
39
|
+
@findings = findings.freeze
|
|
40
|
+
super("specifications contain semantic findings")
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.load(paths, rbs: [], targets: [])
|
|
45
|
+
rbs_paths = Array(rbs)
|
|
46
|
+
selected_targets = Array(targets)
|
|
47
|
+
return Loader.load(paths) if rbs_paths.empty? && selected_targets.empty?
|
|
48
|
+
if rbs_paths.empty?
|
|
49
|
+
raise ConfigurationError.new("RBS targets require at least one RBS file", code: "rbs_without_paths")
|
|
50
|
+
end
|
|
51
|
+
if selected_targets.empty?
|
|
52
|
+
raise ConfigurationError.new("RBS files require at least one selected target", code: "targets_required")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
require_relative "specproof/sources/rbs"
|
|
56
|
+
dsl_paths = normalize_mixed_paths(paths, rbs_paths)
|
|
57
|
+
specifications = dsl_paths.empty? ? [] : Loader.load(dsl_paths, allow_exclusion_only: true)
|
|
58
|
+
declarations = specifications.to_h { |specification| [specification.target, specification] }
|
|
59
|
+
type_facts = Sources::RBS.load(paths: rbs_paths, targets: selected_targets, declarations: declarations)
|
|
60
|
+
Composer.compose(specifications: specifications, type_facts: type_facts)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.normalize_mixed_paths(paths, rbs_paths)
|
|
64
|
+
dsl_paths = Array(paths).map { |path| canonical_path(path) }
|
|
65
|
+
canonical = dsl_paths + rbs_paths.map { |path| canonical_path(path) }
|
|
66
|
+
if canonical.uniq.length != canonical.length
|
|
67
|
+
raise ConfigurationError.new("supplied specification and RBS paths must be unique", code: "duplicate_path")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
dsl_paths.sort.freeze
|
|
71
|
+
end
|
|
72
|
+
private_class_method :normalize_mixed_paths
|
|
73
|
+
|
|
74
|
+
def self.canonical_path(value)
|
|
75
|
+
path = File.path(value)
|
|
76
|
+
File.realpath(path)
|
|
77
|
+
rescue Errno::ENOENT, Errno::EACCES => e
|
|
78
|
+
raise ConfigurationError.new(e.message, code: "file_not_found", path: e.respond_to?(:path) ? e.path : path)
|
|
79
|
+
rescue SystemCallError, TypeError, ArgumentError => e
|
|
80
|
+
raise ConfigurationError.new(e.message, code: "file_error", path: e.respond_to?(:path) ? e.path : path)
|
|
81
|
+
end
|
|
82
|
+
private_class_method :canonical_path
|
|
83
|
+
|
|
84
|
+
def self.check(specifications)
|
|
85
|
+
Checker.new(specifications).findings
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def self.plan(specifications, criterion: "input-basics-v1")
|
|
89
|
+
Planner.new(specifications, criterion: criterion).obligations
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def self.suggest(specifications, report:)
|
|
93
|
+
Suggestions.new(specifications, report: report).result
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
require_relative "specproof/coverage"
|
|
98
|
+
require_relative "specproof/skills"
|
|
99
|
+
|
|
100
|
+
Specproof = SpecProof unless Object.const_defined?(:Specproof, false)
|
data/sig/specproof.rbs
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
module SpecProof
|
|
2
|
+
VERSION: String
|
|
3
|
+
|
|
4
|
+
def self.load: (Array[String] paths, ?rbs: Array[String], ?targets: Array[String]) -> Array[Specification]
|
|
5
|
+
def self.check: (Array[Specification] specifications) -> Array[Finding | CombinationFinding]
|
|
6
|
+
def self.plan: (Array[Specification] specifications, ?criterion: String) -> Array[Obligation | PairObligation]
|
|
7
|
+
def self.suggest: (Array[Specification] specifications, report: Hash[String, untyped]) -> SuggestionResult
|
|
8
|
+
|
|
9
|
+
class Error < StandardError
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class ConfigurationError < Error
|
|
13
|
+
attr_reader code: String
|
|
14
|
+
attr_reader source: Source?
|
|
15
|
+
attr_reader path: String?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class Specification
|
|
19
|
+
attr_reader target: String
|
|
20
|
+
attr_reader inputs: Hash[Symbol, Input]
|
|
21
|
+
attr_reader exclusions: Hash[Symbol, Exclusion]
|
|
22
|
+
attr_reader combination_group: CombinationGroup?
|
|
23
|
+
attr_reader source: Source
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class Exclusion
|
|
27
|
+
attr_reader name: Symbol
|
|
28
|
+
attr_reader reason: String
|
|
29
|
+
attr_reader source: Source
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class Input
|
|
33
|
+
attr_reader name: Symbol
|
|
34
|
+
attr_reader domain: IntegerDomain | FiniteDomain
|
|
35
|
+
attr_reader partitions: Array[Partition]
|
|
36
|
+
attr_reader exhaustive: bool
|
|
37
|
+
attr_reader disjoint: bool
|
|
38
|
+
attr_reader source: Source
|
|
39
|
+
attr_reader type_constraint: ScalarTypeConstraint?
|
|
40
|
+
attr_reader type_sources: Array[Source]
|
|
41
|
+
def sources: () -> Array[Source]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class Partition
|
|
45
|
+
attr_reader name: Symbol
|
|
46
|
+
attr_reader domain: IntegerDomain | FiniteDomain
|
|
47
|
+
attr_reader source: Source
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
class IntegerDomain
|
|
51
|
+
attr_reader lower: Integer
|
|
52
|
+
attr_reader upper: Integer
|
|
53
|
+
def self.from_range: (Range[Integer] range) -> IntegerDomain
|
|
54
|
+
def include?: (untyped value) -> bool
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class FiniteDomain
|
|
58
|
+
attr_reader values: Array[nil | bool | Integer | Symbol | String]
|
|
59
|
+
def include?: (untyped value) -> bool
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
class Source
|
|
63
|
+
attr_reader path: String
|
|
64
|
+
attr_reader line: Integer
|
|
65
|
+
attr_reader kind: String
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
class ScalarTypeConstraint
|
|
69
|
+
attr_reader integer_any: bool
|
|
70
|
+
attr_reader values: Array[nil | bool | Integer | Symbol | String]
|
|
71
|
+
def initialize: (integer_any: bool, values: Array[nil | bool | Integer | Symbol | String]) -> void
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
class InputTypeFact
|
|
75
|
+
attr_reader target: String
|
|
76
|
+
attr_reader input: Symbol
|
|
77
|
+
attr_reader constraint: ScalarTypeConstraint
|
|
78
|
+
attr_reader source: Source
|
|
79
|
+
def initialize: (target: String, input: Symbol, constraint: ScalarTypeConstraint, source: Source) -> void
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
module TypeConstraints
|
|
83
|
+
def self.include?: (ScalarTypeConstraint constraint, untyped value) -> bool
|
|
84
|
+
def self.finite_domain: (ScalarTypeConstraint constraint) -> FiniteDomain?
|
|
85
|
+
def self.first_excluded: (ScalarTypeConstraint constraint, IntegerDomain | FiniteDomain domain) -> untyped
|
|
86
|
+
def self.union: (Array[ScalarTypeConstraint] constraints) -> ScalarTypeConstraint
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
class Finding
|
|
90
|
+
attr_reader code: Symbol
|
|
91
|
+
attr_reader target: String
|
|
92
|
+
attr_reader input: Symbol
|
|
93
|
+
attr_reader witness: untyped
|
|
94
|
+
attr_reader sources: Array[Source]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
class CombinationFinding
|
|
98
|
+
attr_reader code: Symbol
|
|
99
|
+
attr_reader target: String
|
|
100
|
+
attr_reader inputs: Array[Symbol]
|
|
101
|
+
attr_reader sources: Array[Source]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
class Obligation
|
|
105
|
+
attr_reader id: String
|
|
106
|
+
attr_reader target: String
|
|
107
|
+
attr_reader input: Symbol
|
|
108
|
+
attr_reader value: nil | bool | Integer | Symbol | String
|
|
109
|
+
attr_reader reasons: Array[String]
|
|
110
|
+
attr_reader sources: Array[Source]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
class PairObligation
|
|
114
|
+
attr_reader id: String
|
|
115
|
+
attr_reader target: String
|
|
116
|
+
attr_reader inputs: Hash[Symbol, nil | bool | Integer | Symbol | String | PartitionLevel]
|
|
117
|
+
attr_reader reasons: Array[String]
|
|
118
|
+
attr_reader sources: Array[Source]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
class CombinationGroup
|
|
122
|
+
attr_reader inputs: Array[Symbol]
|
|
123
|
+
attr_reader forbidden: Array[ForbiddenAssignment]
|
|
124
|
+
attr_reader source: Source
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
class ForbiddenAssignment
|
|
128
|
+
attr_reader assignments: Hash[Symbol, nil | bool | Integer | Symbol | String | PartitionReference]
|
|
129
|
+
attr_reader source: Source
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
class PartitionReference
|
|
133
|
+
attr_reader name: Symbol
|
|
134
|
+
def initialize: (Symbol name) -> void
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
class CombinationSpace
|
|
138
|
+
attr_reader pairs: Array[Hash[Symbol, nil | bool | Integer | Symbol | String]]
|
|
139
|
+
attr_reader excluded_pairs: Array[Hash[Symbol, nil | bool | Integer | Symbol | String]]
|
|
140
|
+
attr_reader allowed_count: Integer
|
|
141
|
+
attr_reader allowed_assignments: Array[Hash[Symbol, nil | bool | Integer | Symbol | String]]
|
|
142
|
+
attr_reader sources: Array[Source]
|
|
143
|
+
def initialize: (Specification specification) -> void
|
|
144
|
+
def allowed?: (Hash[Symbol, nil | bool | Integer | Symbol | String] arguments) -> bool
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
class PartitionLevel
|
|
148
|
+
attr_reader name: Symbol
|
|
149
|
+
attr_reader lower: Integer
|
|
150
|
+
attr_reader upper: Integer
|
|
151
|
+
def initialize: (name: Symbol, lower: Integer, upper: Integer) -> void
|
|
152
|
+
def include?: (untyped value) -> bool
|
|
153
|
+
def representative: () -> Integer
|
|
154
|
+
def ==: (untyped other) -> bool
|
|
155
|
+
def eql?: (untyped other) -> bool
|
|
156
|
+
def hash: () -> Integer
|
|
157
|
+
def sort_key: () -> Array[untyped]
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
class PartitionSpace
|
|
161
|
+
attr_reader pairs: Array[Hash[Symbol, nil | bool | Integer | Symbol | String | PartitionLevel]]
|
|
162
|
+
attr_reader excluded_pairs: Array[Hash[Symbol, nil | bool | Integer | Symbol | String | PartitionLevel]]
|
|
163
|
+
attr_reader allowed_count: Integer
|
|
164
|
+
attr_reader allowed_assignments: Array[Hash[Symbol, nil | bool | Integer | Symbol | String | PartitionLevel]]
|
|
165
|
+
attr_reader sources: Array[Source]
|
|
166
|
+
def self.for_specification: (Specification specification, ?criterion: String?, ?validate_geometry: bool) -> (PartitionSpace | CombinationSpace)
|
|
167
|
+
def initialize: (Specification specification, ?validate_geometry: bool) -> void
|
|
168
|
+
def allowed?: (Hash[Symbol, nil | bool | Integer | Symbol | String] arguments) -> bool
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
class CoverageReport
|
|
172
|
+
attr_reader contract_digest: String
|
|
173
|
+
attr_reader missing_ids: Array[String]
|
|
174
|
+
attr_reader run_id: String
|
|
175
|
+
attr_reader tests_passed: bool
|
|
176
|
+
attr_reader scope: Hash[String, untyped]
|
|
177
|
+
attr_reader scope_snapshot: Hash[String, untyped]
|
|
178
|
+
attr_reader criterion: String
|
|
179
|
+
def initialize: (Hash[String, untyped] report, specifications: Array[Specification], obligations: Array[Obligation | PairObligation], ?criterion: String) -> void
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
class Suggestion
|
|
183
|
+
attr_reader id: String
|
|
184
|
+
attr_reader target: String
|
|
185
|
+
attr_reader inputs: Hash[Symbol, nil | bool | Integer | Symbol | String]
|
|
186
|
+
attr_reader covers: Array[String]
|
|
187
|
+
attr_reader unassigned_inputs: Array[Symbol]
|
|
188
|
+
attr_reader sources: Array[Source]
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
class SuggestionResult
|
|
192
|
+
attr_reader suggestions: Array[Suggestion]
|
|
193
|
+
attr_reader contract_digest: String
|
|
194
|
+
attr_reader source_run_id: String
|
|
195
|
+
attr_reader source_tests_passed: bool
|
|
196
|
+
attr_reader source_scope: Hash[String, untyped]
|
|
197
|
+
attr_reader missing_count: Integer
|
|
198
|
+
attr_reader criterion: String
|
|
199
|
+
attr_reader strategy: String
|
|
200
|
+
def initialize: (suggestions: Array[Suggestion], contract_digest: String, source_run_id: String, source_tests_passed: bool, source_scope: Hash[String, untyped], missing_count: Integer, ?criterion: String, ?strategy: String) -> void
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def self.define: (String | Method | UnboundMethod target) { () -> void } -> Specification
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
module SpecProof::TargetName
|
|
207
|
+
def self.normalize: (String | Method | UnboundMethod target, ?source: SpecProof::Source?) -> String
|
|
208
|
+
def self.parse: (String target) -> [String, Symbol, :instance | :singleton]
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
module Specproof
|
|
212
|
+
VERSION: String
|
|
213
|
+
def self.load: (Array[String] paths, ?rbs: Array[String], ?targets: Array[String]) -> Array[SpecProof::Specification]
|
|
214
|
+
def self.check: (Array[SpecProof::Specification] specifications) -> Array[SpecProof::Finding | SpecProof::CombinationFinding]
|
|
215
|
+
def self.plan: (Array[SpecProof::Specification] specifications, ?criterion: String) -> Array[SpecProof::Obligation | SpecProof::PairObligation]
|
|
216
|
+
def self.suggest: (Array[SpecProof::Specification] specifications, report: Hash[String, untyped]) -> SpecProof::SuggestionResult
|
|
217
|
+
def self.define: (String | Method | UnboundMethod target) { () -> void } -> SpecProof::Specification
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
module SpecProof::Minitest
|
|
221
|
+
def self.install!: (specifications: Array[String], ?rbs: Array[String], ?targets: Array[String], ?output: String, ?criterion: String, ?fail_on_missing: bool) -> self
|
|
222
|
+
end
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: specproof-specify
|
|
3
|
+
description: Creates and reviews explicit SpecProof input specifications for Ruby methods. Use when a task asks to define input domains or partitions, check gaps and overlaps, plan boundary candidates, or suggest allowed assignments for missing pairwise coverage.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Specify Ruby method inputs with SpecProof
|
|
7
|
+
|
|
8
|
+
Create a bounded input contract from stated requirements and verify its
|
|
9
|
+
partitions with the project's SpecProof command line. Keep the contract tied
|
|
10
|
+
to independent requirements and the implementation source. Do not infer an
|
|
11
|
+
intended policy from a method's current behavior.
|
|
12
|
+
|
|
13
|
+
## Workflow
|
|
14
|
+
|
|
15
|
+
1. Read the user request, relevant product requirements, the target Ruby method,
|
|
16
|
+
and its existing tests. Identify the canonical target (`Owner#method` for an
|
|
17
|
+
instance method, `Owner.method` for a class/module singleton method) and each
|
|
18
|
+
requested input. Distinguish explicit requirements from assumptions.
|
|
19
|
+
When RBS is supplied, inspect explicitly selected RBS target signatures
|
|
20
|
+
first, including explicit `def self.method` selectors on named classes and
|
|
21
|
+
modules. Reuse supported finite input types directly;
|
|
22
|
+
add a DSL declaration only for a behavioral range or partition the type
|
|
23
|
+
cannot express. Do not search for other signatures or duplicate finite
|
|
24
|
+
values in the DSL.
|
|
25
|
+
2. If an input's allowed domain or partition intent is missing, ask a focused
|
|
26
|
+
question before declaring it. If requirements and implementation disagree,
|
|
27
|
+
report the mismatch with source locations; do not rewrite the requirement or
|
|
28
|
+
implementation to make the check green without direction.
|
|
29
|
+
3. Add a small trusted Ruby specification file. By default, every named
|
|
30
|
+
runtime parameter must have an analyzed `input` declaration. If requirements
|
|
31
|
+
intentionally limit this task to only some inputs, declare each remaining
|
|
32
|
+
named parameter with `exclude_input :name, reason: "..."`; exclusions must
|
|
33
|
+
be explicit, named, and justified by scope. There is no wildcard or
|
|
34
|
+
implicit exclusion, and at least one input must remain analyzed. An exclusion
|
|
35
|
+
does not add obligations or excuse an unsupported selected input. For a
|
|
36
|
+
selected RBS target, finite supported inputs can be imported without
|
|
37
|
+
duplicate DSL values; DSL exclusions can account for deliberately
|
|
38
|
+
out-of-scope or unsupported type-only parameters. Unsupported selected union
|
|
39
|
+
types remain atomic failures. RBS string literals and finite unions of
|
|
40
|
+
supported string literals are exact finite scenarios; unrestricted RBS
|
|
41
|
+
`String` and unions containing unsupported members remain unsupported. Do
|
|
42
|
+
not infer a finite range for unrestricted
|
|
43
|
+
`Integer`, guess from a name or implementation, or write partial obligations
|
|
44
|
+
for unsupported input types or union members. Stop and report the selected
|
|
45
|
+
target/input and source location whenever a type is unsupported or needs a
|
|
46
|
+
bounded refinement. Finite DSL strings are available only when a fresh
|
|
47
|
+
`check` and `plan` from the installed build
|
|
48
|
+
accept and report the string kind; a `0.1.x` version alone does not establish
|
|
49
|
+
support. If a required string value is unsupported, preserve it in the
|
|
50
|
+
reported request and stop. Do not substitute a same-spelling symbol, drop
|
|
51
|
+
the string, or report an invalid/partial plan as valid.
|
|
52
|
+
4. Mark a partition family `allow_gaps: false` only when the requirements say it
|
|
53
|
+
covers the entire input domain. Mark it `allow_overlaps: false` only when the
|
|
54
|
+
requirements say the partitions cannot overlap. Leave either promise out
|
|
55
|
+
when the requirement is unknown, then ask about the missing policy rather
|
|
56
|
+
than choosing a boundary. The options default to `true` (gaps/overlaps
|
|
57
|
+
permitted); `false` requires the corresponding check. They describe the
|
|
58
|
+
partitions, not which values the tests must execute. Prefer these spellings
|
|
59
|
+
for new declarations. When migrating, `exhaustive: true` becomes
|
|
60
|
+
`allow_gaps: false` and `disjoint: true` becomes `allow_overlaps: false`;
|
|
61
|
+
invert `false` to `true` as well. Legacy spellings remain accepted, but do
|
|
62
|
+
not supply both spellings for the same policy.
|
|
63
|
+
5. Run `specproof check PATH --format json` and inspect the structured
|
|
64
|
+
`findings`, `errors`, and `contract_digest`. For RBS, pass each selected file
|
|
65
|
+
with `--rbs FILE` and each target with `--target 'Class#method'`; a target
|
|
66
|
+
selector is required and imports only that target. Unsupported union arms
|
|
67
|
+
are atomic failures: do not guess, drop an arm, or turn the remainder into a
|
|
68
|
+
partial contract. Resolve or report every finding before planning. After
|
|
69
|
+
the check is clean, run `specproof plan` with the same source and target
|
|
70
|
+
options. Use the default `input-basics-v1` criterion for marginal input
|
|
71
|
+
candidates. Select `--criterion pairwise-finite-v1` only when requirements
|
|
72
|
+
describe finite joint scenarios and every target has an explicit group. Use
|
|
73
|
+
`pairwise-partitions-v1` only for integer ranges with explicit complete,
|
|
74
|
+
non-overlapping partitions crossed with finite inputs and an all-allowed group.
|
|
75
|
+
Compare the criterion, candidate values or typed pairs, reasons, IDs, and
|
|
76
|
+
source locations with the stated contract. Optional parameters and nullable
|
|
77
|
+
types are distinct: optionality permits omission, while `T?` permits an
|
|
78
|
+
effective `nil` value after Ruby applies defaults.
|
|
79
|
+
6. Report the contract digest, candidate values, and declaration source
|
|
80
|
+
locations. If inputs are excluded, report each name and reason. Explain any
|
|
81
|
+
unresolved question. Describe the plan as boundary candidates, not as a
|
|
82
|
+
complete or proven test suite.
|
|
83
|
+
|
|
84
|
+
Use the project's established command environment. For a Bundler-managed
|
|
85
|
+
project, prefix commands with `bundle exec`. For JSON output and schema details,
|
|
86
|
+
read [the bundled command and report reference](references/usage.md).
|
|
87
|
+
|
|
88
|
+
Choose the target representation for the work: use a canonical string when
|
|
89
|
+
checking or planning offline, when the application owner may not be loaded, or
|
|
90
|
+
when avoiding autoload is important. Strings normalize without loading the
|
|
91
|
+
owner. Use `Owner.instance_method(:name)` or `Owner.method(:name)` when the
|
|
92
|
+
named owner is already loaded and a method object is clearer; SpecProof uses
|
|
93
|
+
reflection and does not call it. Object targets are accepted only for directly
|
|
94
|
+
defined public methods on named classes/modules. An unbound instance method
|
|
95
|
+
names its defining class; aliases, inherited singleton methods, anonymous
|
|
96
|
+
owners, and bound methods on arbitrary instances are unsupported. Validate
|
|
97
|
+
method objects against the current definition when declaring them. Equivalent
|
|
98
|
+
string/object forms share the canonical target identity, digest, and
|
|
99
|
+
obligation IDs. Reports keep string target names, and editor navigation from
|
|
100
|
+
those names depends on the language server.
|
|
101
|
+
|
|
102
|
+
## Finite joint scenarios
|
|
103
|
+
|
|
104
|
+
Use `combinations` only when the independent requirements describe a bounded
|
|
105
|
+
joint input scenario. For `pairwise-finite-v1`, all group inputs must have
|
|
106
|
+
finite analyzed domains. A group names at least two unique inputs and can
|
|
107
|
+
select a subset of analyzed inputs. Each analyzed input is still checked
|
|
108
|
+
before a runtime tuple counts.
|
|
109
|
+
Choose exactly one explicit mode:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
combinations :expedited, :notify, all_allowed: true
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
This asserts that every tuple is allowed; it does not say the inputs are
|
|
116
|
+
behaviorally independent. If requirements rule out assignments, encode the
|
|
117
|
+
stated rule as keyword data:
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
combinations :expedited, :notify do
|
|
121
|
+
forbid expedited: true, notify: false
|
|
122
|
+
end
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Never add a `forbid` merely to erase a missing pair, and never use a Ruby block
|
|
126
|
+
as a predicate. Rules are explicit assignments, validated against the finite
|
|
127
|
+
domains. A pair is planned only when it appears in an allowed full assignment;
|
|
128
|
+
do not decide feasibility by looking at its values in isolation. A forbidden
|
|
129
|
+
three-input tuple may contain pairs that remain feasible through another
|
|
130
|
+
allowed value of the third input. Pairwise planning rejects oversized spaces
|
|
131
|
+
instead of sampling. A valid group with no allowed tuple is a semantic finding
|
|
132
|
+
that must be resolved against requirements.
|
|
133
|
+
|
|
134
|
+
For `pairwise-partitions-v1`, each grouped integer input must declare a
|
|
135
|
+
complete, non-overlapping partition of its bounded range. Other grouped inputs
|
|
136
|
+
use finite values as singleton categories; explicit partitions on finite-valued
|
|
137
|
+
inputs remain unsupported. Choose `all_allowed: true` only when every full
|
|
138
|
+
assignment is allowed. Otherwise, use a nonempty block of requirement-backed
|
|
139
|
+
`forbid` assignments. Integer selectors must name whole declared partitions
|
|
140
|
+
with `in_partition(:name)`, while finite inputs use exact values. Restrictions
|
|
141
|
+
are partial assignments over full tuples: required and excluded pairs depend on
|
|
142
|
+
whether any allowed completion exists, including assignments to other inputs.
|
|
143
|
+
Never add a restriction to hide an uncovered pair. This criterion plans
|
|
144
|
+
category pairs, not boundary candidates; category coverage does not establish
|
|
145
|
+
boundary coverage or equivalent behavior throughout a category.
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
SpecProof.define "AgePolicy#eligible?" do
|
|
149
|
+
input :age, range: 0..120, allow_gaps: false, allow_overlaps: false do
|
|
150
|
+
partition :minor, range: 0...18
|
|
151
|
+
partition :adult, range: 18..120
|
|
152
|
+
end
|
|
153
|
+
input :country, values: ["ro", "de"]
|
|
154
|
+
combinations :age, :country, all_allowed: true
|
|
155
|
+
end
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Select `--criterion pairwise-partitions-v1` explicitly when planning this
|
|
159
|
+
contract. The scalar and `pairwise-finite-v1` criteria keep their existing
|
|
160
|
+
behavior.
|
|
161
|
+
|
|
162
|
+
When requirements restrict tuples, replace `all_allowed: true` with explicit
|
|
163
|
+
clauses, for example:
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
combinations :age, :account, :country do
|
|
167
|
+
forbid age: in_partition(:minor), account: "business"
|
|
168
|
+
end
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
The CLI criterion is explicit and does not change the default scalar plan:
|
|
172
|
+
|
|
173
|
+
```sh
|
|
174
|
+
specproof plan spec/delivery.rb --criterion pairwise-finite-v1 --format json
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`check` remains criterion-independent. A pairwise plan needs a group for every
|
|
178
|
+
supplied target; it will not silently omit targets without joint declarations.
|
|
179
|
+
Read the usage reference before interpreting the pair-row `inputs` object and
|
|
180
|
+
`excluded_pairs` list.
|
|
181
|
+
|
|
182
|
+
After an intended Minitest run, `specproof suggest PATH --report FILE` can
|
|
183
|
+
propose allowed full group assignments for missing pairs. It selects the
|
|
184
|
+
pairwise criterion from the validated coverage report; do not pass a criterion
|
|
185
|
+
to `suggest`. Use it only with a
|
|
186
|
+
freshly inspected, completed and intact coverage report whose current digest,
|
|
187
|
+
typed obligation denominator, input scope, counters, and missing/exercised
|
|
188
|
+
projections agree with a new pairwise plan. A false `tests_passed` value is
|
|
189
|
+
valid evidence and is preserved; it means the assertions failed in that run.
|
|
190
|
+
Filtered reports describe only their recorded scope. The digest does not
|
|
191
|
+
fingerprint implementation or tests, so collect a fresh report after either
|
|
192
|
+
changes. Suggestions are not executed and include no expected result. Validate
|
|
193
|
+
each candidate against independent behavior requirements before adding an
|
|
194
|
+
assertion. Keep every out-of-group or explicitly excluded parameter in the
|
|
195
|
+
reported `unassigned_inputs` list and supply a valid value for each when
|
|
196
|
+
writing a call. Never add a `forbid` or exclusion to make a missing-pair report
|
|
197
|
+
look complete.
|
|
198
|
+
|
|
199
|
+
For partition-pair suggestions, the typed plan row identifies category names
|
|
200
|
+
and inclusive bounds, while each suggested concrete input uses the partition's
|
|
201
|
+
lower endpoint. This representative is a call value, not a boundary-testing
|
|
202
|
+
claim or an assertion oracle.
|
|
203
|
+
|
|
204
|
+
## Supported declarations
|
|
205
|
+
|
|
206
|
+
SpecProof accepts bounded integer ranges and finite arrays of `nil`, booleans,
|
|
207
|
+
integers, and symbols. Builds with finite-string support also accept explicitly
|
|
208
|
+
listed text values. Integer range endpoints are inclusive unless Ruby's
|
|
209
|
+
exclusive range form (`...`) is used. For example, `values: [nil, 1,
|
|
210
|
+
"user_123"]` describes those three exact candidates; it is a finite scenario,
|
|
211
|
+
not the full String domain. String and symbol values with identical spelling
|
|
212
|
+
are distinct. Strings must be exact `String` instances encoded as valid UTF-8
|
|
213
|
+
or US-ASCII; US-ASCII is canonicalized to UTF-8. Empty, whitespace, Unicode,
|
|
214
|
+
and embedded-NUL strings are valid. Invalid byte sequences, other encodings,
|
|
215
|
+
and String subclasses are rejected. No trimming, case folding, or Unicode
|
|
216
|
+
normalization occurs. A specification declares a target and one or more named
|
|
217
|
+
inputs. Partition membership, gap, and overlap checks also apply to
|
|
218
|
+
explicitly enumerated string values; strings are not
|
|
219
|
+
coerced to symbols or other scalar types:
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
require "specproof"
|
|
223
|
+
|
|
224
|
+
SpecProof.define "Registration#adult?" do
|
|
225
|
+
input :age, range: 0..120, allow_gaps: false, allow_overlaps: false do
|
|
226
|
+
partition :minor, range: 0...18
|
|
227
|
+
partition :adult, range: 18..120
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Keep the declared method signature aligned with the real named positional
|
|
233
|
+
or keyword inputs, including optional parameters. By default, declare every
|
|
234
|
+
parameter with `input`. To scope a contract to only some parameters, account
|
|
235
|
+
for every other name with an explicit `exclude_input` and a reason:
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
SpecProof.define "DeliveryPolicy#accept?" do
|
|
239
|
+
input :expedited, values: [false, true]
|
|
240
|
+
exclude_input :payload, reason: "Payload content is outside this option coverage"
|
|
241
|
+
end
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
No input may disappear through a wildcard or omission, and a contract must
|
|
245
|
+
analyze at least one input. Domains describe effective values at method entry,
|
|
246
|
+
after Ruby applies defaults; they do not assert whether the caller omitted an
|
|
247
|
+
argument. Excluded values are not read by runtime collection; selected
|
|
248
|
+
non-primitive values still make collection incomplete. Do not evaluate or guess
|
|
249
|
+
default expressions while authoring the domain. SpecProof copies and freezes
|
|
250
|
+
owned declaration and entry-observation snapshots, so later application
|
|
251
|
+
mutation cannot change matching and application-owned strings are not frozen.
|
|
252
|
+
Direct Coverage observation also owns retained string witnesses; an
|
|
253
|
+
outside-domain string witness is full-sized even though witness count is
|
|
254
|
+
bounded.
|
|
255
|
+
|
|
256
|
+
## Explicit RBS inputs
|
|
257
|
+
|
|
258
|
+
For an explicitly selected RBS target, `bool`, `nil`, supported scalar
|
|
259
|
+
literals, and finite unions provide finite input candidates without a duplicate
|
|
260
|
+
DSL declaration. A resolved built-in `::Integer` remains unbounded and requires
|
|
261
|
+
an explicit finite range or values before SpecProof can produce candidates.
|
|
262
|
+
Existing DSL range and partition declarations are behavioral refinements, not
|
|
263
|
+
claims inferred from RBS. Check that every declared value is admitted by the
|
|
264
|
+
imported type; a contradiction is a conflict to resolve, not an intersection
|
|
265
|
+
to silently accept. An explicit DSL exclusion can scope out an otherwise
|
|
266
|
+
unsupported type-only input while finite selected inputs remain imported; do
|
|
267
|
+
not exclude a member of an unsupported selected union to make that union appear
|
|
268
|
+
supported. Return types do not supply expected results, behavior
|
|
269
|
+
rules, or an output oracle. The report digest fingerprints normalized input
|
|
270
|
+
domains, partition policies, and sorted excluded input names, not exclusion
|
|
271
|
+
reasons/source locations, all declaration text, a full RBS signature, or
|
|
272
|
+
implementation correctness. Existing no-exclusion digests and obligation IDs
|
|
273
|
+
remain stable. Run fresh checks and reports after any input source changes.
|
|
274
|
+
RBS string literals, finite unions of supported literals, and optional literal
|
|
275
|
+
forms provide exact finite string scenarios without duplicate DSL inputs.
|
|
276
|
+
Unrestricted RBS `String` and selected unions containing unsupported members
|
|
277
|
+
fail atomically. Return types remain ignored and do not supply an expected
|
|
278
|
+
result. DSL finite-string support does not widen the RBS adapter to unrestricted
|
|
279
|
+
strings. Verify support from a fresh plan; do not infer it from `0.1.x`.
|
|
280
|
+
Load specification files only from trusted project sources;
|
|
281
|
+
they are executable Ruby. SpecProof does not load the application class while
|
|
282
|
+
checking or planning.
|