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
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.