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,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "minitest/autorun"
5
+ require "specproof/minitest"
6
+
7
+ mode = ARGV.shift || "correct"
8
+ output = ARGV.shift || File.expand_path("../../tmp/age-policy-#{mode}.json", __dir__)
9
+ tests = {
10
+ "correct" => "test/age_policy_test.rb",
11
+ "missing" => "faulty/test/age_policy_examples_test.rb",
12
+ "regression" => "faulty/test/age_policy_test.rb"
13
+ }.freeze
14
+ abort "Choose correct, missing, or regression" unless tests.key?(mode)
15
+
16
+ implementation = mode == "correct" ? "lib/age_policy.rb" : "faulty/lib/age_policy.rb"
17
+ require File.join(__dir__, implementation)
18
+ SpecProof::Minitest.install!(
19
+ specifications: [File.join(__dir__, "specification.rb")],
20
+ output: output,
21
+ fail_on_missing: ENV["SPECPROOF_STRICT"] == "1"
22
+ )
23
+ require File.join(__dir__, tests.fetch(mode))
@@ -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...18
6
+ partition :adult, range: 18..120
7
+ end
8
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require_relative "../lib/age_policy"
5
+
6
+ # Verifies the declared age policy examples.
7
+ class AgePolicyTest < Minitest::Test
8
+ def test_adult_boundary
9
+ policy = AgePolicy.new
10
+
11
+ refute policy.adult?(0)
12
+ refute policy.adult?(1)
13
+ refute policy.adult?(17)
14
+ assert policy.adult?(18)
15
+ assert policy.adult?(119)
16
+ assert policy.adult?(120)
17
+ end
18
+ end
@@ -0,0 +1,40 @@
1
+ # Constrained pairwise coverage over integer partitions
2
+
3
+ This example combines the complete, non-overlapping age categories `minor`
4
+ (`0...18`) and `adult` (`18..120`) with finite account and country values. Its
5
+ [independent requirements](requirements.md) prohibit minors from having a
6
+ business account and define eligibility from age alone. The restriction uses
7
+ `in_partition(:minor)` to select the whole integer category:
8
+
9
+ ```ruby
10
+ combinations :age, :account, :country do
11
+ forbid age: in_partition(:minor), account: "business"
12
+ end
13
+ ```
14
+
15
+ Run from the `gems/specproof` directory with the prepared Ruby and bundle for
16
+ this checkout. Reports and suggestion output go under `tmp/`:
17
+
18
+ ```sh
19
+ bundle exec ruby -Ilib exe/specproof plan examples/constrained_partition_pairs/specification.rb --criterion pairwise-partitions-v1 --format json
20
+ bundle exec ruby examples/constrained_partition_pairs/run.rb initial
21
+ bundle exec ruby -Ilib exe/specproof suggest examples/constrained_partition_pairs/specification.rb --report tmp/constrained-partition-pairs-initial.json --format json > tmp/constrained-partition-pairs-suggestions.json
22
+ bundle exec ruby examples/constrained_partition_pairs/run.rb complete
23
+ ```
24
+
25
+ The two integer categories and two finite domains produce eight possible full
26
+ assignments. The requirement-backed restriction removes the two assignments
27
+ with a minor and a business account, leaving six allowed tuples. The plan has
28
+ eleven required category-pair obligations and one `excluded_pairs` entry with
29
+ `reason: "no_allowed_completion"`. The initial process passes three requirement-
30
+ backed calls and hits nine obligations. `suggest` proposes the two remaining
31
+ allowed assignments, `age: 18, account: "business", country: "de"` and
32
+ `age: 0, account: "personal", country: "de"`. The complete process adds those
33
+ calls with expected results justified by the separate requirement.
34
+
35
+ The normalized selector value used for the contract digest is
36
+ `{ "kind": "partition_reference", "name": "minor" }`. Planned pair values
37
+ continue to use the existing partition object with its name and inclusive
38
+ bounds. Suggestions choose each partition's lower endpoint as a concrete call
39
+ input. Neither category coverage nor a suggested input establishes boundary
40
+ coverage or an expected result.
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Applies the age requirement to supported account and country values.
4
+ class AccountEligibility
5
+ def eligible?(age:, account:, country:)
6
+ %w[personal business].include?(account) && %w[ro de].include?(country) && age >= 18
7
+ end
8
+ end
@@ -0,0 +1,11 @@
1
+ # Account eligibility requirements
2
+
3
+ An account may be opened only by an adult. Minors cannot have a business
4
+ account. Both supported account types (`"personal"` and `"business"`) and
5
+ countries (`"ro"` and `"de"`) are otherwise valid. Eligibility is true exactly
6
+ when age is at least 18; account type and country do not change that result.
7
+
8
+ The restriction on business accounts justifies forbidding the whole `minor`
9
+ partition with `account: "business"`. The age-based eligibility rule supplies
10
+ expected outputs to the example tests. Suggestions provide inputs only and
11
+ are not 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/account_eligibility"
7
+
8
+ mode = ARGV.shift || "initial"
9
+ abort "Choose initial or complete" unless %w[initial complete].include?(mode)
10
+
11
+ ENV["ADD_MISSING_PAIRS"] = mode == "complete" ? "1" : "0"
12
+ root = File.expand_path("../..", __dir__)
13
+ report = File.join(root, "tmp/constrained-partition-pairs-#{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_account_eligibility"
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "specproof"
4
+
5
+ SpecProof.define "AccountEligibility#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 :account, values: %w[personal business]
12
+ input :country, values: %w[ro de]
13
+
14
+ combinations :age, :account, :country do
15
+ forbid age: in_partition(:minor), account: "business"
16
+ end
17
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require_relative "lib/account_eligibility"
5
+
6
+ # Covers selected age-category, account, and country pairs.
7
+ class AccountEligibilityPairwiseTest < Minitest::Test
8
+ BASELINE_CASES = [
9
+ [16, "personal", "ro", false],
10
+ [30, "business", "ro", true],
11
+ [30, "personal", "de", true]
12
+ ].freeze
13
+
14
+ def test_baseline_calls_follow_the_age_requirement
15
+ policy = AccountEligibility.new
16
+
17
+ BASELINE_CASES.each do |age, account, country, expected|
18
+ assert_equal expected,
19
+ policy.eligible?(age: age, account: account, country: country),
20
+ "requirements.md: eligibility depends on age >= 18, not account or country"
21
+ end
22
+ end
23
+
24
+ if ENV["ADD_MISSING_PAIRS"] == "1"
25
+ def test_suggested_assignments_follow_the_age_requirement
26
+ policy = AccountEligibility.new
27
+
28
+ assert_equal false, policy.eligible?(age: 0, account: "personal", country: "de"),
29
+ "requirements.md: every minor is ineligible"
30
+ assert_equal true, policy.eligible?(age: 18, account: "business", country: "de"),
31
+ "requirements.md: every adult with a supported account and country is eligible"
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,22 @@
1
+ # Explicit input selection
2
+
3
+ This example analyzes the boolean `expedited` option and explicitly excludes
4
+ the ordinary `Hash` payload. The expected results come from the independent
5
+ [delivery requirements](requirements.md), not from the implementation.
6
+
7
+ From the `gems/specproof` checkout, inspect the offline contract and candidate
8
+ plan, then run the Minitest suite:
9
+
10
+ ```sh
11
+ bundle exec ruby -Ilib exe/specproof check examples/input_selection/specification.rb --format json
12
+ bundle exec ruby -Ilib exe/specproof plan examples/input_selection/specification.rb --format json
13
+ bundle exec ruby -Ilib examples/input_selection/test_delivery_policy.rb
14
+ ```
15
+
16
+ The plan has one candidate for each boolean value. The suite asserts both
17
+ acceptance and rejection under the independent rules and passes ordinary Hash
18
+ values at runtime. Its coverage report concerns only the selected `expedited`
19
+ input. It says nothing about payload values, payload content, output behavior
20
+ outside the assertions, interactions, or complete method correctness. An
21
+ excluded input cannot be used to claim coverage for that input. If a selected
22
+ input receives a non-primitive value, collection is incomplete.
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Evaluates whether a delivery meets its option requirements.
4
+ class DeliveryPolicy
5
+ def accept?(payload, expedited:)
6
+ return false unless payload.fetch(:ready)
7
+ return true unless expedited
8
+
9
+ payload.fetch(:priority) == :high
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ # Delivery option requirements
2
+
3
+ The option decision is evaluated only for ready deliveries. A regular ready
4
+ delivery is accepted regardless of payload details. An expedited ready delivery
5
+ is accepted exactly when its payload has `priority: :high`. A delivery that is
6
+ not ready is rejected.
7
+
8
+ These rules are the expected-result oracle for the example tests. They describe
9
+ behavior independently of `DeliveryPolicy#accept?`.
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "specproof"
4
+
5
+ SpecProof.define "DeliveryPolicy#accept?" do
6
+ input :expedited, values: [false, true]
7
+ exclude_input :payload, reason: "Payload content is outside this option coverage"
8
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/autorun"
4
+ require_relative "delivery_policy"
5
+ require "specproof/minitest"
6
+
7
+ SpecProof::Minitest.install!(
8
+ specifications: ["examples/input_selection/specification.rb"],
9
+ output: "tmp/specproof-input-selection-coverage.json",
10
+ fail_on_missing: true
11
+ )
12
+
13
+ # Exercises delivery options against the independent requirements.
14
+ class DeliveryPolicyTest < Minitest::Test
15
+ def setup
16
+ @policy = DeliveryPolicy.new
17
+ end
18
+
19
+ def test_regular_ready_delivery_is_accepted
20
+ assert @policy.accept?({ ready: true, priority: :low }, expedited: false),
21
+ "A regular ready delivery is accepted (requirements.md)"
22
+ end
23
+
24
+ def test_high_priority_expedited_delivery_is_accepted
25
+ assert @policy.accept?({ ready: true, priority: :high }, expedited: true),
26
+ "A high-priority expedited delivery is accepted (requirements.md)"
27
+ end
28
+
29
+ def test_low_priority_expedited_delivery_is_rejected
30
+ refute @policy.accept?({ ready: true, priority: :low }, expedited: true),
31
+ "A low-priority expedited delivery is rejected (requirements.md)"
32
+ end
33
+
34
+ def test_unready_delivery_is_rejected
35
+ refute @policy.accept?({ ready: false, priority: :high }, expedited: false),
36
+ "An unready delivery is rejected (requirements.md)"
37
+ end
38
+ end
@@ -0,0 +1,30 @@
1
+ # String and method-object targets
2
+
3
+ The requirements in [requirements.md](requirements.md) define independent
4
+ input and behavior rules for an instance method, a class method, and a module
5
+ method. The two declarations below select those same methods in equivalent
6
+ ways and produce the same canonical target identities, contract digest, and
7
+ obligation IDs.
8
+
9
+ Run from the `gems/specproof` directory with the checkout's Ruby and bundle:
10
+
11
+ ```sh
12
+ bundle exec ruby -Ilib exe/specproof plan examples/method_targets/specification_strings.rb --format json > tmp/method-targets-strings.json
13
+ bundle exec ruby -Ilib exe/specproof plan examples/method_targets/specification_objects.rb --format json > tmp/method-targets-objects.json
14
+ ```
15
+
16
+ The first command uses strings and does not load or autoload the application
17
+ owners. The second loads the definitions so Ruby can provide one
18
+ `UnboundMethod` and two `Method` objects; SpecProof inspects those objects
19
+ without invoking the methods. Compare `contract_digest` and the typed
20
+ obligation `id`/`target` pairs in the reports. The target fields remain
21
+ canonical strings, such as `DeliveryOptions#build` and
22
+ `DeliveryDefaults.build`.
23
+
24
+ String declarations are a good fit for offline checking or planning when
25
+ application loading is unavailable or could trigger autoload. Method objects
26
+ are useful in runtime declarations after code has loaded. They must refer to
27
+ directly defined public methods on named classes/modules and remain current at
28
+ declaration time. Aliases, inherited singleton methods, anonymous owners, and
29
+ bound methods on arbitrary instances are unsupported. Editor navigation from
30
+ target names depends on language-server support.
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Provides a region-specific shipping default.
4
+ module DeliveryDefaults
5
+ def self.build(region:)
6
+ { carrier: region == :local ? :nearby : :national }
7
+ end
8
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Builds delivery options for requested quantities and scheduling modes.
4
+ class DeliveryOptions
5
+ def build(quantity:)
6
+ { quantity: quantity, package: quantity.zero? ? :empty : :parcel }
7
+ end
8
+
9
+ def self.build(mode:)
10
+ { mode: mode, schedule_at_checkout: mode == :scheduled }
11
+ end
12
+ end
@@ -0,0 +1,20 @@
1
+ # Independent requirements
2
+
3
+ These rules define the example's expected behavior independently of its Ruby
4
+ implementation:
5
+
6
+ - An instance delivery build accepts quantities from `0` through `100`; a
7
+ zero quantity selects the `:empty` package and every positive quantity
8
+ selects `:parcel` in its result.
9
+ - The class-level delivery builder supports `:scheduled` and `:immediate`;
10
+ scheduling at checkout is enabled only for `:scheduled`.
11
+ - The module-level default builder supports `:local` and `:remote`; local
12
+ delivery selects the `:nearby` carrier and remote delivery selects
13
+ `:national`.
14
+
15
+ The two specification files encode the same input domains and canonical target
16
+ names. `specification_strings.rb` can be planned without loading these methods.
17
+ `specification_objects.rb` loads the file and demonstrates equivalent
18
+ `UnboundMethod`/`Method` selections. The runtime methods exist to make those
19
+ objects available; the requirements above remain the source of expected
20
+ behavior.
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "specproof"
4
+ require_relative "lib/delivery_options"
5
+ require_relative "lib/delivery_defaults"
6
+
7
+ SpecProof.define DeliveryOptions.instance_method(:build) do
8
+ input :quantity, range: 0..100
9
+ end
10
+
11
+ SpecProof.define DeliveryOptions.method(:build) do
12
+ input :mode, values: %i[immediate scheduled]
13
+ end
14
+
15
+ SpecProof.define DeliveryDefaults.method(:build) do
16
+ input :region, values: %i[local remote]
17
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "specproof"
4
+
5
+ SpecProof.define "DeliveryOptions#build" do
6
+ input :quantity, range: 0..100
7
+ end
8
+
9
+ SpecProof.define "DeliveryOptions.build" do
10
+ input :mode, values: %i[immediate scheduled]
11
+ end
12
+
13
+ SpecProof.define "DeliveryDefaults.build" do
14
+ input :region, values: %i[local remote]
15
+ end
@@ -0,0 +1,46 @@
1
+ # Pairwise input scenarios
2
+
3
+ The [requirements](requirements.md) independently say that express speed is
4
+ selected exactly when `expedited` is true, and the notification flag follows
5
+ `notify`. The input declaration describes finite booleans; it does not encode
6
+ those output rules.
7
+
8
+ Run these commands from the `gems/specproof` directory with its development
9
+ bundle:
10
+
11
+ ```sh
12
+ bundle exec ruby -Ilib exe/specproof plan examples/pairwise/specification.rb --criterion pairwise-finite-v1 --format json
13
+ bundle exec ruby examples/pairwise/run.rb initial
14
+ bundle exec ruby examples/pairwise/run.rb gap
15
+ bundle exec ruby examples/pairwise/run.rb corrected
16
+ bundle exec ruby -Ilib exe/specproof plan examples/pairwise/constrained_specification.rb --criterion pairwise-finite-v1 --format json
17
+ bundle exec ruby -Ilib exe/specproof plan examples/pairwise/three_boolean_specification.rb --criterion pairwise-finite-v1 --format json
18
+ bundle exec ruby examples/pairwise/run.rb three
19
+ ```
20
+
21
+ The plan has four pair obligations for the two booleans. The `initial` process
22
+ runs three independent assertions for `00`, `01`, and `10`: all three tests
23
+ pass and coverage is 3/4. The faulty method computes the notification flag as
24
+ `notify && !expedited`; it agrees with the requirement on those three cases.
25
+ The `gap` process repeats them and adds the requirement-backed `11` assertion.
26
+ That assertion fails, although all four pair obligations have now been
27
+ exercised (4/4). The `corrected` process uses a separate implementation and
28
+ passes all four assertions with 4/4 pair coverage. Each mode runs in its own
29
+ process so the implementations never coexist.
30
+
31
+ The constrained declaration has its own [requirement](constrained_requirements.md):
32
+ expedited delivery requires notification. It explicitly forbids
33
+ `expedited: true, notify: false`, so the plan has three required pairs and one
34
+ `excluded_pairs` row with `reason: "no_allowed_completion"`. This rule is not
35
+ used by the unconstrained example and must not be inferred from the faulty
36
+ implementation.
37
+
38
+ The three-boolean declaration has 12 possible pair rows. Four calls, `000`,
39
+ `011`, `101`, and `110`, exercise all 12. That is pair coverage for the three
40
+ input pairs; it does not claim all eight full tuples were run or that all
41
+ application behavior is covered.
42
+
43
+ Each runner mode writes a fresh report below `tmp/pairwise-*.json`. Compare its
44
+ criterion, digest, typed pair rows, run ID, integrity, and `tests_passed` with
45
+ the current plan and command. Coverage records method calls; the assertions
46
+ derive expected values from the separate requirements file.
@@ -0,0 +1,6 @@
1
+ # Constrained delivery requirement
2
+
3
+ For this separate target, expedited delivery requires notification. Therefore
4
+ the full tuple `expedited: true, notify: false` is invalid. The other three
5
+ boolean tuples are allowed. This requirement justifies the explicit forbidden
6
+ assignment in `constrained_specification.rb`.
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "specproof"
4
+
5
+ SpecProof.define "ConstrainedDelivery#build" do
6
+ input :expedited, values: [false, true]
7
+ input :notify, values: [false, true]
8
+ combinations :expedited, :notify do
9
+ forbid expedited: true, notify: false
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Builds the delivery choices from the two option flags.
4
+ class DeliveryOptions
5
+ def build(expedited:, notify:)
6
+ {
7
+ speed: expedited ? :express : :standard,
8
+ notification: notify
9
+ }
10
+ end
11
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Checks the corrected implementation against each stated delivery rule.
4
+ class CorrectedDeliveryOptionsTest < Minitest::Test
5
+ def setup
6
+ @delivery_options = DeliveryOptions.new
7
+ end
8
+
9
+ def test_standard_delivery_without_notification
10
+ assert_equal({ speed: :standard, notification: false },
11
+ @delivery_options.build(expedited: false, notify: false))
12
+ end
13
+
14
+ def test_standard_delivery_with_notification
15
+ assert_equal({ speed: :standard, notification: true },
16
+ @delivery_options.build(expedited: false, notify: true))
17
+ end
18
+
19
+ def test_express_delivery_without_notification
20
+ assert_equal({ speed: :express, notification: false },
21
+ @delivery_options.build(expedited: true, notify: false))
22
+ end
23
+
24
+ def test_express_delivery_with_notification
25
+ assert_equal({ speed: :express, notification: true },
26
+ @delivery_options.build(expedited: true, notify: true))
27
+ end
28
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Builds the delivery choices with the documented interaction defect.
4
+ class DeliveryOptions
5
+ def build(expedited:, notify:)
6
+ {
7
+ speed: expedited ? :express : :standard,
8
+ notification: notify && !expedited
9
+ }
10
+ end
11
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Adds the requirement-backed pair missing from the baseline tests.
4
+ class GapDeliveryOptionsTest < Minitest::Test
5
+ def test_express_delivery_with_notification
6
+ assert_equal({ speed: :express, notification: true },
7
+ DeliveryOptions.new.build(expedited: true, notify: true),
8
+ "Requirement: notification follows notify; see requirements.md")
9
+ end
10
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Covers the three baseline option combinations.
4
+ class InitialDeliveryOptionsTest < Minitest::Test
5
+ def setup
6
+ @delivery_options = DeliveryOptions.new
7
+ end
8
+
9
+ def test_standard_delivery_without_notification
10
+ assert_equal({ speed: :standard, notification: false },
11
+ @delivery_options.build(expedited: false, notify: false))
12
+ end
13
+
14
+ def test_standard_delivery_with_notification
15
+ assert_equal({ speed: :standard, notification: true },
16
+ @delivery_options.build(expedited: false, notify: true))
17
+ end
18
+
19
+ def test_express_delivery_without_notification
20
+ assert_equal({ speed: :express, notification: false },
21
+ @delivery_options.build(expedited: true, notify: false))
22
+ end
23
+ end
@@ -0,0 +1,11 @@
1
+ # Delivery option requirements
2
+
3
+ These are the normative requirements for `DeliveryOptions#build`:
4
+
5
+ 1. Speed is `:express` exactly when `expedited` is true; otherwise it is
6
+ `:standard`.
7
+ 2. The notification flag equals `notify`, independent of the speed selection.
8
+
9
+ Both inputs are booleans. Every combination is valid for this example. The
10
+ faulty and corrected implementations are judged against these rules, not used
11
+ to derive them.
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "minitest/autorun"
5
+ require "specproof/minitest"
6
+
7
+ mode = ARGV.shift || "corrected"
8
+ output = ARGV.shift || File.expand_path("../../tmp/pairwise-#{mode}.json", __dir__)
9
+ implementations = {
10
+ "initial" => ["faulty/lib/delivery_options.rb"],
11
+ "gap" => ["faulty/lib/delivery_options.rb"],
12
+ "corrected" => ["corrected/lib/delivery_options.rb"],
13
+ "three" => ["three_boolean/lib/boolean_batch.rb"]
14
+ }.freeze
15
+ test_files = {
16
+ "initial" => ["faulty/test/initial_delivery_options_test.rb"],
17
+ "gap" => ["faulty/test/initial_delivery_options_test.rb", "faulty/test/gap_delivery_options_test.rb"],
18
+ "corrected" => ["corrected/test/delivery_options_test.rb"],
19
+ "three" => ["three_boolean/test/boolean_batch_test.rb"]
20
+ }.freeze
21
+ specifications = {
22
+ "initial" => "specification.rb",
23
+ "gap" => "specification.rb",
24
+ "corrected" => "specification.rb",
25
+ "three" => "three_boolean_specification.rb"
26
+ }.freeze
27
+ abort "Choose initial, gap, corrected, or three" unless implementations.key?(mode)
28
+
29
+ require File.join(__dir__, implementations.fetch(mode).first)
30
+ SpecProof::Minitest.install!(
31
+ specifications: [File.join(__dir__, specifications.fetch(mode))],
32
+ criterion: "pairwise-finite-v1",
33
+ output: output,
34
+ fail_on_missing: false
35
+ )
36
+ test_files.fetch(mode).each { |path| require File.join(__dir__, path) }
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "specproof"
4
+
5
+ SpecProof.define "DeliveryOptions#build" do
6
+ input :expedited, values: [false, true]
7
+ input :notify, values: [false, true]
8
+ combinations :expedited, :notify, all_allowed: true
9
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Returns the three finite boolean inputs as a batch result.
4
+ class BooleanBatch
5
+ def build(first:, second:, third:)
6
+ [first, second, third]
7
+ end
8
+ end