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,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Exercises all pairs among three boolean inputs with four calls.
4
+ class BooleanBatchTest < Minitest::Test
5
+ def setup
6
+ @batch = BooleanBatch.new
7
+ end
8
+
9
+ def test_all_inputs_false
10
+ assert_equal([false, false, false],
11
+ @batch.build(first: false, second: false, third: false))
12
+ end
13
+
14
+ def test_first_false_other_inputs_true
15
+ assert_equal([false, true, true],
16
+ @batch.build(first: false, second: true, third: true))
17
+ end
18
+
19
+ def test_first_and_third_true
20
+ assert_equal([true, false, true],
21
+ @batch.build(first: true, second: false, third: true))
22
+ end
23
+
24
+ def test_first_and_second_true
25
+ assert_equal([true, true, false],
26
+ @batch.build(first: true, second: true, third: false))
27
+ end
28
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "specproof"
4
+
5
+ SpecProof.define "BooleanBatch#build" do
6
+ input :first, values: [false, true]
7
+ input :second, values: [false, true]
8
+ input :third, values: [false, true]
9
+ combinations :first, :second, :third, all_allowed: true
10
+ end
@@ -0,0 +1,34 @@
1
+ # Pairwise coverage by integer partitions
2
+
3
+ This example declares an age range split into the complete, non-overlapping
4
+ categories `minor` (`0...18`) and `adult` (`18..120`), plus the finite country
5
+ values `"ro"` and `"de"`. All combinations are allowed. The independent
6
+ [requirement](requirements.md) says eligibility requires age 18 or older and
7
+ does not depend on country.
8
+
9
+ Run from the `gems/specproof` directory with the prepared Ruby and bundle for
10
+ this checkout. Reports and suggestion output go under `tmp/`:
11
+
12
+ ```sh
13
+ bundle exec ruby -Ilib exe/specproof plan examples/pairwise_partitions/specification.rb --criterion pairwise-partitions-v1 --format json
14
+ bundle exec ruby examples/pairwise_partitions/run.rb initial
15
+ bundle exec ruby -Ilib exe/specproof suggest examples/pairwise_partitions/specification.rb --report tmp/pairwise-partitions-initial.json --format json > tmp/pairwise-partitions-suggestions.json
16
+ bundle exec ruby examples/pairwise_partitions/run.rb complete
17
+ ```
18
+
19
+ The plan has four category-pair obligations: minor/adult crossed with each
20
+ country. Its typed partition values include a name and inclusive bounds, such
21
+ as `{ "kind": "partition", "name": "minor", "lower": "0", "upper": "17" }`.
22
+ The initial process passes three requirement-backed calls and leaves
23
+ minor/`"de"` unobserved. `suggest` reads the criterion from that validated
24
+ coverage report and proposes the concrete input `age: 0, country: "de"` using
25
+ the partition's lower endpoint. The suggestion supplies an input only. The
26
+ complete process adds a separate assertion, justified by the under-18 rule,
27
+ and exercises all four category pairs.
28
+
29
+ Category coverage counts any in-domain age in the declared category. It is not
30
+ boundary-value coverage, does not establish equivalence within a category, and
31
+ does not infer expected outputs. The assertion oracle remains the separate
32
+ requirement. Existing `pairwise-finite-v1` and scalar plans retain their
33
+ behavior; this criterion supports only integer ranges with explicit complete,
34
+ non-overlapping partitions and `all_allowed: true`.
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Applies the minimum age requirement to supported countries.
4
+ class AgePolicy
5
+ def eligible?(age:, country:)
6
+ %w[ro de].include?(country) && age >= 18
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # Age eligibility requirement
2
+
3
+ For the supported countries `"ro"` and `"de"`, an applicant is eligible
4
+ exactly when their age is at least 18. Country does not change the age rule.
5
+ In particular, every applicant under 18 is ineligible.
6
+
7
+ This requirement supplies the expected results in the example tests. Pairwise
8
+ category coverage and a suggested input do not provide an output oracle.
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "minitest/autorun"
5
+ require "specproof/minitest"
6
+ require_relative "lib/age_policy"
7
+
8
+ mode = ARGV.shift || "initial"
9
+ abort "Choose initial or complete" unless %w[initial complete].include?(mode)
10
+
11
+ ENV["ADD_MISSING_CATEGORY"] = mode == "complete" ? "1" : "0"
12
+ root = File.expand_path("../..", __dir__)
13
+ report = File.join(root, "tmp/pairwise-partitions-#{mode}.json")
14
+
15
+ SpecProof::Minitest.install!(
16
+ specifications: [File.join(__dir__, "specification.rb")],
17
+ criterion: "pairwise-partitions-v1",
18
+ output: report,
19
+ fail_on_missing: false
20
+ )
21
+
22
+ require_relative "test_age_policy"
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "specproof"
4
+
5
+ SpecProof.define "AgePolicy#eligible?" do
6
+ input :age, range: 0..120, allow_gaps: false, allow_overlaps: false do
7
+ partition :minor, range: 0...18
8
+ partition :adult, range: 18..120
9
+ end
10
+
11
+ input :country, values: %w[ro de]
12
+ combinations :age, :country, all_allowed: true
13
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require_relative "lib/age_policy"
5
+
6
+ # Covers selected age-category and country pairs.
7
+ class AgePolicyPairwiseTest < Minitest::Test
8
+ BASELINE_CASES = [
9
+ [16, "ro", false],
10
+ [30, "ro", true],
11
+ [30, "de", true]
12
+ ].freeze
13
+
14
+ def test_baseline_calls_follow_the_age_requirement
15
+ policy = AgePolicy.new
16
+
17
+ BASELINE_CASES.each do |age, country, expected|
18
+ assert_equal expected, policy.eligible?(age: age, country: country),
19
+ "requirements.md: eligibility depends on age >= 18, not country"
20
+ end
21
+ end
22
+
23
+ if ENV["ADD_MISSING_CATEGORY"] == "1"
24
+ def test_minor_from_de_is_ineligible
25
+ refute AgePolicy.new.eligible?(age: 0, country: "de"),
26
+ "requirements.md: every applicant under 18 is ineligible"
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,48 @@
1
+ # Pairwise missing-pair suggestions
2
+
3
+ Run these commands from `gems/specproof` with its development bundle. Reports
4
+ and generated suggestion JSON go under the ignored `tmp/` directory. Each
5
+ runner command starts a new Ruby process and writes a fresh coverage report.
6
+
7
+ ```sh
8
+ bundle exec ruby examples/pairwise_suggestions/run.rb delivery-baseline tmp/suggestion-delivery-baseline.json
9
+ bundle exec ruby -Ilib exe/specproof suggest examples/pairwise/specification.rb --report tmp/suggestion-delivery-baseline.json --format json > tmp/suggestion-delivery.json
10
+ bundle exec ruby examples/pairwise_suggestions/run.rb delivery-gap tmp/suggestion-delivery-gap.json
11
+ bundle exec ruby -Ilib exe/specproof suggest examples/pairwise/specification.rb --report tmp/suggestion-delivery-gap.json --format json > tmp/suggestion-after-gap.json
12
+ bundle exec ruby examples/pairwise_suggestions/run.rb delivery-corrected tmp/suggestion-delivery-corrected.json
13
+ bundle exec ruby examples/pairwise_suggestions/run.rb booleans-baseline tmp/suggestion-booleans-baseline.json
14
+ bundle exec ruby -Ilib exe/specproof suggest examples/pairwise_suggestions/three_boolean/specification.rb --report tmp/suggestion-booleans-baseline.json --format json > tmp/suggestion-booleans.json
15
+ bundle exec ruby examples/pairwise_suggestions/run.rb booleans-complete tmp/suggestion-booleans-complete.json
16
+ ```
17
+
18
+ The DeliveryOptions declaration and [requirements](../pairwise/requirements.md)
19
+ come from the existing pairwise example. Its baseline tests pass while
20
+ exercising three of four pair obligations. The suggestion command returns an
21
+ allowed `expedited: true, notify: true` assignment and the stable ID it covers.
22
+ That row is a candidate only: the example's separate gap test asserts the
23
+ independent requirement that the notification result equals `notify`. The
24
+ assertion fails against the faulty implementation, even though its fresh
25
+ report has exercised all four pairs. The corrected implementation passes that
26
+ same assertion in a separate process and writes a different report.
27
+ Running `suggest` on the failing gap report is also valid: it returns no rows
28
+ because all pairs were reached, while preserving `source_tests_passed: false`.
29
+
30
+ For three booleans, the baseline observes only `false, false, false`, leaving
31
+ nine pair IDs missing. One full assignment in the suggestion covers multiple
32
+ missing pairs. The group requires no joint restrictions, and the independent
33
+ [requirements](three_boolean/requirements.md) define the expected result. The
34
+ suggestion output lists the deliberately excluded `context` input as
35
+ `unassigned_inputs`; callers must still provide it and satisfy its application
36
+ contract. The completion run uses the three remaining assignments from the
37
+ small four-row covering set and asserts the requirement for every call.
38
+
39
+ Never turn suggestion output directly into a test call with generated or
40
+ implementation-derived expected values. Compare the report with a fresh
41
+ pairwise plan: it must be completed and intact, with the current digest, exact
42
+ typed obligation denominator and scope, consistent counters and projections,
43
+ and no errors or unknown observations. A failed assertion result is accepted
44
+ and preserved. Filtered reports remain limited to their recorded scope. The
45
+ digest does not fingerprint implementation or tests, so run the suite again
46
+ after any such changes. The CLI validates the report before producing any
47
+ suggestions; oversized or inconsistent evidence yields an error without
48
+ partial results.
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "minitest/autorun"
5
+ require "specproof/minitest"
6
+
7
+ mode = ARGV.shift || "delivery-corrected"
8
+ output = ARGV.shift || File.expand_path("../../tmp/pairwise-suggestions-#{mode}.json", __dir__)
9
+
10
+ pairwise_root = File.expand_path("../pairwise", __dir__)
11
+ scenarios = {
12
+ "delivery-baseline" => {
13
+ implementation: "faulty/lib/delivery_options.rb",
14
+ specification: "specification.rb",
15
+ tests: ["faulty/test/initial_delivery_options_test.rb"]
16
+ },
17
+ "delivery-gap" => {
18
+ implementation: "faulty/lib/delivery_options.rb",
19
+ specification: "specification.rb",
20
+ tests: [
21
+ "faulty/test/initial_delivery_options_test.rb",
22
+ "faulty/test/gap_delivery_options_test.rb"
23
+ ]
24
+ },
25
+ "delivery-corrected" => {
26
+ implementation: "corrected/lib/delivery_options.rb",
27
+ specification: "specification.rb",
28
+ tests: ["corrected/test/delivery_options_test.rb"]
29
+ },
30
+ "booleans-baseline" => {
31
+ implementation: "pairwise_suggestions/three_boolean/lib/suggested_boolean_batch.rb",
32
+ specification: "pairwise_suggestions/three_boolean/specification.rb",
33
+ tests: ["pairwise_suggestions/three_boolean/test/baseline_test.rb"]
34
+ },
35
+ "booleans-complete" => {
36
+ implementation: "pairwise_suggestions/three_boolean/lib/suggested_boolean_batch.rb",
37
+ specification: "pairwise_suggestions/three_boolean/specification.rb",
38
+ tests: ["pairwise_suggestions/three_boolean/test/complete_test.rb"]
39
+ }
40
+ }.freeze
41
+
42
+ abort "Choose one of: #{scenarios.keys.join(", ")}" unless scenarios.key?(mode)
43
+
44
+ scenario = scenarios.fetch(mode)
45
+ root = mode.start_with?("delivery-") ? pairwise_root : File.expand_path("..", __dir__)
46
+ require File.join(root, scenario.fetch(:implementation))
47
+
48
+ SpecProof::Minitest.install!(
49
+ specifications: [File.join(root, scenario.fetch(:specification))],
50
+ criterion: "pairwise-finite-v1",
51
+ output: output,
52
+ fail_on_missing: false
53
+ )
54
+
55
+ scenario.fetch(:tests).each { |path| require File.join(root, path) }
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Returns three finite boolean inputs as an ordered array.
4
+ class SuggestedBooleanBatch
5
+ def build(first:, second:, third:, context:) # rubocop:disable Lint/UnusedMethodArgument
6
+ [first, second, third]
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ # Boolean batch requirements
2
+
3
+ `SuggestedBooleanBatch#build` returns the supplied three boolean values in
4
+ `[first, second, third]` order. Every triple is permitted. `context` is an
5
+ application value outside this finite pairwise scenario; callers must supply a
6
+ value appropriate for their application. The pairwise suggestions specify
7
+ neither that value nor an expected return value.
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "specproof"
4
+
5
+ SpecProof.define "SuggestedBooleanBatch#build" do
6
+ input :first, values: [false, true]
7
+ input :second, values: [false, true]
8
+ input :third, values: [false, true]
9
+ exclude_input :context, reason: "Context behavior is outside this boolean interaction example"
10
+ combinations :first, :second, :third, all_allowed: true
11
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Verifies the single tuple observed before pairwise suggestions.
4
+ class SuggestedBooleanBatchBaselineTest < Minitest::Test
5
+ def test_all_false_returns_the_input_values_in_order
6
+ assert_equal([false, false, false],
7
+ SuggestedBooleanBatch.new.build(first: false, second: false, third: false, context: Object.new))
8
+ end
9
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Checks a compact covering set against the independent array requirement.
4
+ class SuggestedBooleanBatchCompleteTest < Minitest::Test
5
+ def setup
6
+ @batch = SuggestedBooleanBatch.new
7
+ end
8
+
9
+ def test_each_requirement_backed_assignment_returns_values_in_order
10
+ [
11
+ [false, false, false],
12
+ [false, true, true],
13
+ [true, false, true],
14
+ [true, true, false]
15
+ ].each do |values|
16
+ first, second, third = values
17
+ assert_equal(values,
18
+ @batch.build(first: first, second: second, third: third, context: Object.new),
19
+ "Requirement: preserve boolean input order; see requirements.md")
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ SpecProof.define "AgePolicy#adult?" do
4
+ input :age, range: 0..120, allow_gaps: false, allow_overlaps: false do
5
+ partition :minor, range: 0..17
6
+ partition :adult, range: 18..64
7
+ end
8
+ end
@@ -0,0 +1,58 @@
1
+ # RBS-backed registration example
2
+
3
+ This example imports `age: Integer` and optional nullable `consent: bool?`
4
+ from [the explicit RBS signature](sig/registration.rbs), then uses the DSL
5
+ only to refine age to `0..120` with complete, non-overlapping minor/adult
6
+ partitions. The independent rule is in [requirements.md](requirements.md): an
7
+ applicant is eligible exactly when age is at least 18 and consent is true.
8
+ Missing consent and explicit false both deny eligibility.
9
+
10
+ The composed plan has nine marginal obligations: six age candidates
11
+ (`0`, `1`, `17`, `18`, `119`, `120`) and three consent values (`nil`, `false`,
12
+ `true`). The nine obligations do not mean nine combinations and do not state
13
+ any expected return value.
14
+
15
+ Run all commands from the gem directory. The RBS parser is in the explicit
16
+ optional bundle. Every command starts a fresh Ruby process. Offline check and
17
+ plan emit JSON to stdout; the three runtime checkpoints below write separate
18
+ coverage reports under `/private/tmp`:
19
+
20
+ ```sh
21
+ cd gems/specproof
22
+ BUNDLE_GEMFILE=Gemfile.rbs bundle exec ruby -Ilib exe/specproof check examples/rbs_registration/specification.rb --rbs examples/rbs_registration/sig/registration.rbs --target 'Registration#eligible?' --format json
23
+ BUNDLE_GEMFILE=Gemfile.rbs bundle exec ruby -Ilib exe/specproof plan examples/rbs_registration/specification.rb --rbs examples/rbs_registration/sig/registration.rbs --target 'Registration#eligible?' --format json
24
+ ```
25
+
26
+ The baseline implementation uses `age > 18`. It tests every declared age
27
+ candidate except 18, across each consent value. Its meaningful assertions
28
+ follow the written eligibility rule, and its fresh report should show eight of
29
+ nine marginal obligations exercised, with only the age-18 obligation missing:
30
+
31
+ ```sh
32
+ SPEC_PROOF_REPORT=/private/tmp/registration-baseline.json CHECKPOINT=baseline BUNDLE_GEMFILE=Gemfile.rbs bundle exec ruby -I examples/rbs_registration/implementations/age_gt examples/rbs_registration/test_registration.rb
33
+ ```
34
+
35
+ Run the added age-18 assertion alongside the passing baseline cases against
36
+ that same faulty implementation. The assertion should fail because an
37
+ 18-year-old who consents is eligible under the stated rule. The report should
38
+ show all nine candidates hit but `tests_passed: false`, demonstrating that
39
+ input coverage alone does not establish correctness:
40
+
41
+ ```sh
42
+ SPEC_PROOF_REPORT=/private/tmp/registration-boundary-red.json CHECKPOINT=boundary BUNDLE_GEMFILE=Gemfile.rbs bundle exec ruby -I examples/rbs_registration/implementations/age_gt examples/rbs_registration/test_registration.rb
43
+ ```
44
+
45
+ The final checkpoint loads the separate corrected implementation (`age >= 18`)
46
+ and tests all six age candidates with all three consent values. Its new report
47
+ should have a distinct run ID and all nine unchanged obligations exercised:
48
+
49
+ ```sh
50
+ SPEC_PROOF_REPORT=/private/tmp/registration-corrected.json CHECKPOINT=complete BUNDLE_GEMFILE=Gemfile.rbs bundle exec ruby -I examples/rbs_registration/implementations/age_gte examples/rbs_registration/test_registration.rb
51
+ ```
52
+
53
+ Inspect each fresh report's digest, obligation IDs, run ID, completion,
54
+ integrity, `tests_passed`, and declaration scope. The digest covers normalized
55
+ input domains and partition policy; it does not fingerprint all source text,
56
+ the full signature, or implementation correctness. A passing input-coverage
57
+ report is evidence that candidates reached the method, while the behavior
58
+ assertions and their independent rule establish expected results.
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Registration example with the deliberate age-greater-than defect.
4
+ class Registration
5
+ def eligible?(age:, consent: nil)
6
+ age > 18 && consent == true
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Registration example with the corrected inclusive adulthood boundary.
4
+ class Registration
5
+ def eligible?(age:, consent: nil)
6
+ age >= 18 && consent == true
7
+ end
8
+ end
@@ -0,0 +1,9 @@
1
+ # Registration eligibility rule
2
+
3
+ An applicant is eligible exactly when they are at least 18 years old and have
4
+ given consent. Applicants younger than 18 are ineligible. Missing consent and
5
+ explicitly false consent both mean the applicant has not given consent and are
6
+ ineligible. An 18-year-old who consents is eligible.
7
+
8
+ This is the expected-result rule for the example tests. The RBS signature only
9
+ describes accepted input types; it does not provide this behavior rule.
@@ -0,0 +1,3 @@
1
+ class Registration
2
+ def eligible?: (age: Integer, ?consent: bool?) -> bool
3
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "specproof"
4
+
5
+ SpecProof.define "Registration#eligible?" do
6
+ input :age, range: 0..120, allow_gaps: false, allow_overlaps: false do
7
+ partition :minor, range: 0...18
8
+ partition :adult, range: 18..120
9
+ end
10
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require "registration"
5
+ require "specproof/minitest"
6
+
7
+ REPORT = ENV.fetch("SPEC_PROOF_REPORT", "/private/tmp/specproof-registration-#{Process.pid}.json")
8
+ RBS_PATH = File.join(__dir__, "sig/registration.rbs")
9
+ SPECIFICATION = File.join(__dir__, "specification.rb")
10
+
11
+ SpecProof::Minitest.install!(
12
+ specifications: [SPECIFICATION],
13
+ rbs: [RBS_PATH],
14
+ targets: ["Registration#eligible?"],
15
+ output: REPORT,
16
+ fail_on_missing: false
17
+ )
18
+
19
+ # Exercises only explicit RBS-backed input obligations for the example method.
20
+ class RegistrationExampleTest < Minitest::Test
21
+ CONSENT_VALUES = [nil, false, true].freeze
22
+ COMPLETE_AGES = [0, 1, 17, 18, 119, 120].freeze
23
+ GAP_AGES = [0, 1, 17, 119, 120].freeze
24
+
25
+ if ENV["CHECKPOINT"] == "boundary"
26
+ def test_age_18_with_consent_is_eligible
27
+ assert Registration.new.eligible?(age: 18, consent: true),
28
+ "requirements.md: an 18-year-old who consents is eligible"
29
+ end
30
+ end
31
+
32
+ def test_declared_age_and_consent_candidates_follow_the_eligibility_rule
33
+ ages = %w[baseline boundary].include?(ENV["CHECKPOINT"]) ? GAP_AGES : COMPLETE_AGES
34
+
35
+ ages.product(CONSENT_VALUES).each do |age, consent|
36
+ expected = age >= 18 && consent == true
37
+ message = "requirements.md: eligible iff age >= 18 and consent is true " \
38
+ "(age=#{age}, consent=#{consent.inspect})"
39
+ assert_equal expected, Registration.new.eligible?(age: age, consent: consent),
40
+ message
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,34 @@
1
+ # RBS finite string literal example
2
+
3
+ This example imports `country: ("ro" | "de")` and `expedited: bool` from the
4
+ explicit RBS signature. The DSL declares only that every combination is
5
+ allowed; it does not duplicate either input. The independent
6
+ [requirement](requirements.md) says `build` preserves the requested country and
7
+ uses `express` exactly when expedited is true.
8
+
9
+ Run from the gem directory. The RBS parser is in the optional `Gemfile.rbs`
10
+ bundle. Each command starts a fresh Ruby process and writes its report under
11
+ `tmp/`:
12
+
13
+ ```sh
14
+ BUNDLE_GEMFILE=Gemfile.rbs bundle exec ruby -Ilib exe/specproof plan examples/rbs_string_literals/specification.rb --rbs examples/rbs_string_literals/sig/delivery_options.rbs --target 'DeliveryOptions#build' --criterion pairwise-finite-v1 --format json
15
+ BUNDLE_GEMFILE=Gemfile.rbs bundle exec ruby examples/rbs_string_literals/test_delivery_options.rb
16
+ BUNDLE_GEMFILE=Gemfile.rbs bundle exec ruby -Ilib exe/specproof suggest examples/rbs_string_literals/specification.rb --rbs examples/rbs_string_literals/sig/delivery_options.rbs --target 'DeliveryOptions#build' --report tmp/rbs-string-baseline.json --format json
17
+ SPEC_PROOF_REPORT=tmp/rbs-string-complete.json CHECK_GAP=1 BUNDLE_GEMFILE=Gemfile.rbs bundle exec ruby examples/rbs_string_literals/test_delivery_options.rb
18
+ ```
19
+
20
+ The first coverage run passes three requirement-backed calls: `ro` with each
21
+ boolean value and `de` with false. Its fresh report leaves the pair
22
+ `country: "de", expedited: true` unobserved. `suggest` proposes that allowed
23
+ assignment; the suggestion supplies an input only, with no expected result.
24
+ The final fresh run adds a separate assertion justified by the requirement and
25
+ exercises the missing assignment. The example uses the correct implementation
26
+ throughout; it demonstrates a coverage gap without manufacturing a behavior
27
+ defect. Inspect both reports' run IDs, digests, completion, integrity, test
28
+ results, and input scope.
29
+
30
+ RBS `String` is unrestricted and remains unsupported. A selected union with an
31
+ unsupported member also fails atomically. Return types do not provide an
32
+ expected-result oracle. Finite-string support is feature-specific, not
33
+ guaranteed by a `0.1.x` version number; confirm typed string obligations in a
34
+ fresh plan from the installed build.
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Builds delivery labels from explicit country and shipping options.
4
+ class DeliveryOptions
5
+ def build(country:, expedited:)
6
+ shipping = expedited ? "express" : "standard"
7
+ "#{country}:#{shipping}"
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ # Delivery options requirement
2
+
3
+ For the supported country values `"ro"` and `"de"`,
4
+ `DeliveryOptions#build(country:, expedited:)` preserves the supplied country
5
+ string in its result. It selects `"express"` when `expedited` is true and
6
+ `"standard"` when it is false. The country and expedited choice are independent
7
+ options, so every combination is allowed.
8
+
9
+ This requirement is the expected-result source for the example's assertions.
10
+ The RBS signature describes accepted input values only; its return type is not
11
+ used as a behavior oracle.
@@ -0,0 +1,3 @@
1
+ class DeliveryOptions
2
+ def build: (country: ("ro" | "de"), expedited: bool) -> String
3
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "specproof"
4
+
5
+ SpecProof.define "DeliveryOptions#build" do
6
+ combinations :country, :expedited, all_allowed: true
7
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require_relative "lib/delivery_options"
5
+ require "specproof/minitest"
6
+
7
+ ROOT = File.expand_path("../..", __dir__)
8
+ SPECIFICATION = File.join(__dir__, "specification.rb")
9
+ RBS_SIGNATURE = File.join(__dir__, "sig/delivery_options.rbs")
10
+ REPORT = ENV.fetch("SPEC_PROOF_REPORT", File.join(ROOT, "tmp/rbs-string-baseline.json"))
11
+
12
+ SpecProof::Minitest.install!(
13
+ specifications: [SPECIFICATION],
14
+ rbs: [RBS_SIGNATURE],
15
+ targets: ["DeliveryOptions#build"],
16
+ criterion: "pairwise-finite-v1",
17
+ output: REPORT,
18
+ fail_on_missing: false
19
+ )
20
+
21
+ # Exercises the RBS-backed delivery options contract.
22
+ class DeliveryOptionsExampleTest < Minitest::Test
23
+ BASELINE_ASSIGNMENTS = [
24
+ ["ro", false],
25
+ ["ro", true],
26
+ ["de", false]
27
+ ].freeze
28
+
29
+ def test_baseline_assignments_follow_delivery_requirement
30
+ delivery = DeliveryOptions.new
31
+
32
+ BASELINE_ASSIGNMENTS.each do |country, expedited|
33
+ expected_shipping = expedited ? "express" : "standard"
34
+ expected = "#{country}:#{expected_shipping}"
35
+ assert_equal expected, delivery.build(country: country, expedited: expedited),
36
+ "requirements.md: preserve country and select shipping by expedited"
37
+ end
38
+ end
39
+
40
+ if ENV["CHECK_GAP"] == "1"
41
+ def test_suggested_de_expedited_assignment_follows_requirement
42
+ assert_equal "de:express", DeliveryOptions.new.build(country: "de", expedited: true),
43
+ "requirements.md: preserve de and select express when expedited"
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ SpecProof.define "Shipping#available?" do
4
+ input :country, values: %i[ca gb us], allow_gaps: false, allow_overlaps: false do
5
+ partition :supported, values: %i[ca gb us]
6
+ end
7
+
8
+ input :expedited, values: [false, true] do
9
+ partition :standard, values: [false]
10
+ partition :express, values: [true]
11
+ end
12
+ end
data/exe/specproof ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "specproof"
5
+ require "specproof/cli"
6
+
7
+ exit SpecProof::CLI.new.run(ARGV)