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,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SpecProof
4
+ # Finds contradictions between declared domains and partitions.
5
+ class Checker
6
+ def initialize(specifications, criterion: nil, check_combinations: true)
7
+ @specifications = specifications
8
+ @criterion = criterion
9
+ @check_combinations = check_combinations
10
+ end
11
+
12
+ def findings
13
+ results = []
14
+ @specifications.each do |specification|
15
+ specification.inputs.each_value do |input|
16
+ check_input(specification, input, results)
17
+ end
18
+ check_combinations(specification, results) if @check_combinations && specification.combination_group
19
+ end
20
+ results.sort_by do |finding|
21
+ if finding.is_a?(CombinationFinding)
22
+ [finding.target, finding.inputs.map(&:to_s), finding.code.to_s]
23
+ else
24
+ [finding.target, [finding.input.to_s], finding.code.to_s, sortable_witness(finding.witness)]
25
+ end
26
+ end.freeze
27
+ end
28
+
29
+ private
30
+
31
+ def check_combinations(specification, results)
32
+ space = combination_space(specification)
33
+ return unless space.allowed_count.zero?
34
+
35
+ results << CombinationFinding.new(code: :unsatisfiable_combinations, target: specification.target,
36
+ inputs: specification.combination_group.inputs, sources: space.sources)
37
+ end
38
+
39
+ def combination_space(specification)
40
+ PartitionSpace.for_specification(specification, criterion: @criterion, validate_geometry: false)
41
+ end
42
+
43
+ def check_input(specification, input, results)
44
+ domain = input.domain
45
+ if Domains.empty?(domain)
46
+ results << finding(:empty_domain, specification, input, Domains.to_a(domain), input.sources)
47
+ end
48
+ input.partitions.each do |partition|
49
+ if Domains.empty?(partition.domain)
50
+ results << finding(:empty_partition, specification, input, Domains.to_a(partition.domain), [partition.source])
51
+ else
52
+ check_containment(specification, input, partition, results)
53
+ end
54
+ end
55
+ if domain.is_a?(IntegerDomain)
56
+ check_integer_family(specification, input, results) unless Domains.empty?(domain)
57
+ else
58
+ check_finite_family(specification, input, results)
59
+ end
60
+ check_type_domain(specification, input, results) if input.type_constraint
61
+ end
62
+
63
+ def check_containment(specification, input, partition, results)
64
+ domain = input.domain
65
+ child = partition.domain
66
+ outside = if domain.is_a?(IntegerDomain)
67
+ ranges = []
68
+ ranges << [child.lower, [child.upper, domain.lower - 1].min] if child.lower < domain.lower
69
+ ranges << [[child.lower, domain.upper + 1].max, child.upper] if child.upper > domain.upper
70
+ ranges.reject { |left, right| left > right }
71
+ else
72
+ child.values.reject { |value| domain.values.include?(value) }
73
+ end
74
+ return if outside.empty?
75
+
76
+ witness = outside.first
77
+ results << finding(:partition_outside_domain, specification, input, witness, input.sources + [partition.source])
78
+ end
79
+
80
+ def check_integer_family(specification, input, results)
81
+ partitions = input.partitions.select { |part| part.domain.is_a?(IntegerDomain) && !Domains.empty?(part.domain) }
82
+ if input.disjoint
83
+ partitions.each_with_index do |left, index|
84
+ partitions.drop(index + 1).each do |right|
85
+ lower = [left.domain.lower, right.domain.lower].max
86
+ upper = [left.domain.upper, right.domain.upper].min
87
+ next if lower > upper
88
+
89
+ results << finding(:partition_overlap, specification, input, lower, [left.source, right.source])
90
+ end
91
+ end
92
+ end
93
+ return unless input.exhaustive
94
+
95
+ cursor = input.domain.lower
96
+ partitions.map(&:domain).sort_by { |part| [part.lower, part.upper] }.each do |part|
97
+ lower = [part.lower, input.domain.lower].max
98
+ upper = [part.upper, input.domain.upper].min
99
+ next if lower > upper
100
+
101
+ results << finding(:partition_gap, specification, input, [cursor, lower - 1], input.sources) if cursor < lower
102
+ cursor = [cursor, upper + 1].max
103
+ end
104
+ return unless cursor <= input.domain.upper
105
+
106
+ results << finding(:partition_gap, specification, input, [cursor, input.domain.upper], input.sources)
107
+ end
108
+
109
+ def check_finite_family(specification, input, results)
110
+ partitions = input.partitions.select { |part| part.domain.is_a?(FiniteDomain) && !part.domain.values.empty? }
111
+ if input.disjoint
112
+ partitions.each_with_index do |left, index|
113
+ partitions.drop(index + 1).each do |right|
114
+ common = left.domain.values & right.domain.values
115
+ common.each do |value|
116
+ results << finding(:partition_overlap, specification, input, value, [left.source, right.source])
117
+ end
118
+ end
119
+ end
120
+ end
121
+ return unless input.exhaustive
122
+
123
+ covered = partitions.flat_map { |part| part.domain.values }.uniq
124
+ (input.domain.values - covered).each do |value|
125
+ results << finding(:partition_gap, specification, input, value, input.sources)
126
+ end
127
+ end
128
+
129
+ def check_type_domain(specification, input, results)
130
+ witness = TypeConstraints.first_excluded(input.type_constraint, input.domain)
131
+ return if witness.equal?(TypeConstraints::NO_EXCLUSION)
132
+
133
+ results << finding(:type_domain_conflict, specification, input, witness, input.sources)
134
+ end
135
+
136
+ def finding(code, specification, input, witness, sources)
137
+ Finding.new(code: code, target: specification.target, input: input.name, witness: witness, sources: sources)
138
+ end
139
+
140
+ def sortable_witness(witness)
141
+ witness.is_a?(Array) ? [0, *witness] : [1, *Domains.sort_key(witness)]
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,292 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "suggestion_report"
5
+
6
+ module SpecProof
7
+ # Parses command arguments and dispatches SpecProof commands.
8
+ class CLI
9
+ USAGE = <<~TEXT
10
+ Usage:
11
+ specproof --version
12
+ specproof check [SPECIFICATION.rb ...] [--rbs FILE] [--target TARGET] [--format human|json]
13
+ specproof plan [SPECIFICATION.rb ...] [--rbs FILE] [--target TARGET] [--criterion NAME] [--format human|json]
14
+ specproof suggest SPECIFICATION.rb ... --report COVERAGE.json [--rbs FILE --target TARGET] [--format human|json]
15
+ specproof skills export --to DIRECTORY [--format human|json]
16
+ TEXT
17
+
18
+ def initialize(stdout: $stdout, stderr: $stderr)
19
+ @stdout = stdout
20
+ @stderr = stderr
21
+ end
22
+
23
+ def run(argv)
24
+ args = argv.dup
25
+ if [["--version"], ["-v"]].include?(args)
26
+ @stdout.puts("SpecProof #{VERSION}")
27
+ return 0
28
+ end
29
+ if args.empty? || args == ["--help"] || args == ["-h"] || args == ["help"]
30
+ @stdout.write(USAGE)
31
+ return 0
32
+ end
33
+
34
+ command = args.shift
35
+ return run_skills(args) if command == "skills"
36
+ return run_specifications(command, args) if %w[check plan suggest].include?(command)
37
+
38
+ unknown_command(args, "unknown_command", "unsupported command #{command.inspect}", kind: "error")
39
+ end
40
+
41
+ private
42
+
43
+ def run_skills(args)
44
+ unless args.shift == "export"
45
+ return unknown_command(args, "unknown_skills_command", "expected skills export", kind: "skills_export")
46
+ end
47
+
48
+ destination, format, option_error = parse_skill_options(args)
49
+ return emit_error(option_error, format, kind: "skills_export") if option_error
50
+
51
+ with_errors("skills_export", format) do
52
+ result = Skills.export(to: destination)
53
+ if format == "json"
54
+ @stdout.puts(JSON.generate(Report.envelope(kind: "skills_export").merge(result)))
55
+ elsif result.fetch(:exported).empty?
56
+ @stdout.puts("Skills already match the export at #{result.fetch(:destination)}.")
57
+ else
58
+ @stdout.puts("Exported #{result.fetch(:exported).length} skill files to #{result.fetch(:destination)}.")
59
+ end
60
+ 0
61
+ end
62
+ end
63
+
64
+ def run_specifications(kind, args)
65
+ paths, rbs_paths, targets, criterion, format, option_error, report_path = parse_options(args, kind: kind)
66
+ report_kind = kind == "suggest" ? "suggestions" : kind
67
+ if option_error
68
+ return emit_error(option_error, format, kind: report_kind,
69
+ criterion: kind == "plan" ? criterion : nil)
70
+ end
71
+ if paths.empty? && rbs_paths.empty? && targets.empty?
72
+ return emit_error(error("paths_required", "provide specification paths or an RBS selection"), format,
73
+ kind: report_kind, criterion: kind == "plan" ? criterion : nil)
74
+ end
75
+
76
+ with_errors(report_kind, format, criterion: kind == "plan" ? criterion : nil) do
77
+ specifications = with_loader_output(format) do
78
+ if rbs_paths.empty? && targets.empty?
79
+ SpecProof.load(paths)
80
+ else
81
+ SpecProof.load(paths, rbs: rbs_paths, targets: targets)
82
+ end
83
+ end
84
+ case kind
85
+ when "check" then run_check(specifications, format)
86
+ when "plan" then run_plan(specifications, format, criterion)
87
+ else SuggestionReport.execute(specifications: specifications, path: report_path, format: format,
88
+ stdout: @stdout)
89
+ end
90
+ end
91
+ end
92
+
93
+ def run_check(specifications, format)
94
+ findings = SpecProof.check(specifications)
95
+ if format == "json"
96
+ @stdout.puts(JSON.generate(Report.check(specifications: specifications, findings: findings)))
97
+ else
98
+ if findings.empty?
99
+ @stdout.puts("No specification findings.")
100
+ else
101
+ findings.each { |finding| @stdout.puts(Report.finding(finding)) }
102
+ end
103
+ @stdout.puts(Report.human_scope(specifications))
104
+ end
105
+ findings.empty? ? 0 : 1
106
+ end
107
+
108
+ def run_plan(specifications, format, criterion)
109
+ obligations = SpecProof.plan(specifications, criterion: criterion)
110
+ if format == "json"
111
+ @stdout.puts(JSON.generate(Report.plan(specifications: specifications, obligations: obligations,
112
+ criterion: criterion)))
113
+ else
114
+ @stdout.puts(Report.human_plan(obligations, criterion: criterion, specifications: specifications))
115
+ end
116
+ 0
117
+ rescue InvalidSpecificationError => e
118
+ if format == "json"
119
+ payload = Report.plan(specifications: specifications, obligations: [], criterion: criterion)
120
+ payload[:findings] = e.findings.map { |finding| Report.finding_hash(finding) }
121
+ @stdout.puts(JSON.generate(payload))
122
+ else
123
+ e.findings.each { |finding| @stdout.puts(Report.finding(finding)) }
124
+ @stdout.puts(Report.human_scope(specifications))
125
+ end
126
+ 1
127
+ end
128
+
129
+ def unknown_command(args, code, message, kind:)
130
+ return emit_error(error(code, message), "json", kind: kind) if json_format_requested?(args)
131
+
132
+ @stderr.write(USAGE)
133
+ 2
134
+ end
135
+
136
+ def with_errors(kind, format, criterion: nil)
137
+ yield
138
+ rescue ConfigurationError => e
139
+ emit_error(error(e.code, e.message, path: e.path, source: e.source), format,
140
+ kind: kind, criterion: criterion)
141
+ rescue SyntaxError => e
142
+ emit_error(error("syntax_error", e.message, path: e.path), format, kind: kind, criterion: criterion)
143
+ rescue SystemCallError, IOError => e
144
+ emit_error(error("io_error", e.message), format, kind: kind, criterion: criterion)
145
+ rescue StandardError => e
146
+ emit_error(error("specification_error", e.message), format, kind: kind, criterion: criterion)
147
+ end
148
+
149
+ def parse_options(args, kind:)
150
+ options = { format: "human", criterion: "input-basics-v1", criterion_seen: false,
151
+ paths: [], rbs_paths: [], targets: [], report_paths: [], parse_error: nil }
152
+ while (argument = args.shift)
153
+ parse_error = parse_option(argument, args, options, kind)
154
+ options[:parse_error] ||= parse_error
155
+ end
156
+ if kind == "suggest" && options[:report_paths].empty?
157
+ options[:parse_error] ||= error("missing_coverage_report", "suggest requires one --report COVERAGE.json")
158
+ end
159
+ [options[:paths], options[:rbs_paths], options[:targets], options[:criterion], options[:format],
160
+ options[:parse_error], options[:report_paths].first]
161
+ end
162
+
163
+ def parse_option(argument, args, options, kind)
164
+ case argument
165
+ when "--format" then parse_format_option(args, options)
166
+ when "--criterion" then parse_criterion_option(args, options, kind)
167
+ when "--report" then SuggestionReport.parse_report_option(args: args, options: options, kind: kind)
168
+ when "--rbs", "--target" then parse_selection_option(argument, args, options)
169
+ else parse_path_option(argument, options)
170
+ end
171
+ end
172
+
173
+ def parse_format_option(args, options)
174
+ value = args.shift
175
+ if %w[human json].include?(value)
176
+ options[:format] = value
177
+ nil
178
+ else
179
+ args.unshift(value) if value
180
+ error("invalid_format", "--format requires human or json")
181
+ end
182
+ end
183
+
184
+ def parse_criterion_option(args, options, kind)
185
+ value, parse_error = option_value("--criterion", args)
186
+ return parse_error unless value
187
+ return error("unsupported_option", "--criterion is available only for plan") unless kind == "plan"
188
+ return error("duplicate_criterion", "specify --criterion only once") if options[:criterion_seen]
189
+
190
+ options[:criterion] = value
191
+ options[:criterion_seen] = true
192
+ nil
193
+ end
194
+
195
+ def parse_selection_option(argument, args, options)
196
+ value, parse_error = option_value(argument, args)
197
+ return parse_error unless value
198
+
199
+ options.fetch(argument == "--rbs" ? :rbs_paths : :targets) << value
200
+ nil
201
+ end
202
+
203
+ def option_value(argument, args)
204
+ value = args.shift
205
+ if value.nil? || value.empty? || value.start_with?("-")
206
+ args.unshift(value) if value
207
+ [nil, error("missing_option_value", "#{argument} requires a value")]
208
+ else
209
+ [value, nil]
210
+ end
211
+ end
212
+
213
+ def parse_path_option(argument, options)
214
+ if argument.start_with?("-")
215
+ error("unsupported_option", "unsupported option #{argument.inspect}")
216
+ else
217
+ options[:paths] << argument
218
+ nil
219
+ end
220
+ end
221
+
222
+ def json_format_requested?(args)
223
+ index = args.index("--format")
224
+ index && args[index + 1] == "json"
225
+ end
226
+
227
+ def parse_skill_options(args)
228
+ destination = nil
229
+ format = "human"
230
+ parse_error = nil
231
+ destination_seen = false
232
+
233
+ while (arg = args.shift)
234
+ case arg
235
+ when "--to"
236
+ value = args.shift
237
+ if destination_seen
238
+ parse_error ||= error("duplicate_destination", "specify --to only once")
239
+ elsif value.nil? || value.start_with?("--")
240
+ args.unshift(value) if value
241
+ parse_error ||= error("missing_skills_destination", "--to requires a destination")
242
+ else
243
+ destination = value
244
+ destination_seen = true
245
+ end
246
+ when "--format"
247
+ value = args.shift
248
+ if %w[human json].include?(value)
249
+ format = value
250
+ else
251
+ parse_error ||= error("invalid_format", "--format requires human or json")
252
+ end
253
+ else
254
+ parse_error ||= error("unsupported_option", "unsupported skill export argument #{arg.inspect}")
255
+ end
256
+ end
257
+
258
+ unless destination || parse_error
259
+ parse_error = error("missing_skills_destination", "provide an explicit destination with --to")
260
+ end
261
+ [destination, format, parse_error]
262
+ end
263
+
264
+ def with_loader_output(format)
265
+ return yield unless format == "json"
266
+
267
+ original = $stdout
268
+ $stdout = @stderr
269
+ yield
270
+ ensure
271
+ $stdout = original if format == "json"
272
+ end
273
+
274
+ def error(code, message, path: nil, source: nil)
275
+ result = { code: code.to_s, message: message.to_s }
276
+ result[:path] = path if path
277
+ result[:source] = Report.source(source) if source
278
+ result
279
+ end
280
+
281
+ def emit_error(details, format, kind: "error", criterion: nil)
282
+ if format == "json"
283
+ payload = Report.envelope(kind: kind, errors: [details])
284
+ payload[:criterion] = criterion if criterion
285
+ @stdout.puts(JSON.generate(payload))
286
+ else
287
+ @stderr.puts("#{details[:code]}: #{details[:message]}")
288
+ end
289
+ 2
290
+ end
291
+ end
292
+ end