haveapi 0.30.1 → 0.31.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/Gemfile +1 -0
  4. data/benchmarks/Gemfile +30 -0
  5. data/benchmarks/Gemfile.lock +296 -0
  6. data/benchmarks/documentation.rb +277 -0
  7. data/benchmarks/endpoints.rb +786 -0
  8. data/benchmarks/performance_budgets.json +1354 -0
  9. data/benchmarks/performance_compare.rb +678 -0
  10. data/benchmarks/shell.nix +21 -0
  11. data/doc/client-examples.md +179 -26
  12. data/doc/performance.md +310 -0
  13. data/haveapi.gemspec +1 -1
  14. data/lib/haveapi/client_examples/ts_generated_client.rb +36 -0
  15. data/lib/haveapi/documentation_clients.rb +49 -6
  16. data/lib/haveapi/documentation_context.rb +42 -12
  17. data/lib/haveapi/version.rb +1 -1
  18. data/lib/haveapi/views/version_page.erb +2 -2
  19. data/spec/client_examples/context_performance_spec.rb +72 -0
  20. data/spec/client_examples/context_spec.rb +206 -0
  21. data/spec/client_examples/profile_mount_spec.rb +45 -0
  22. data/spec/client_examples/ts_generated_client_spec.rb +53 -0
  23. data/spec/client_examples/typescript_profiles_spec.rb +113 -0
  24. data/spec/documentation/client_examples_spec.rb +2 -2
  25. data/spec/documentation/performance_spec.rb +79 -0
  26. data/spec/performance/comparator_spec.rb +472 -0
  27. data/spec/performance/endpoint_contract_spec.rb +226 -0
  28. data/spec/performance/endpoint_scaling_spec.rb +82 -0
  29. data/spec/performance/event_baseline_spec.rb +407 -0
  30. data/spec/performance/json_activation_spec.rb +151 -0
  31. data/spec/performance/release_lineage_spec.rb +470 -0
  32. data/spec/performance/runner_spec.rb +610 -0
  33. data/spec/performance/sqlite_spec.rb +194 -0
  34. data/spec/performance/support/live_worker.rb +44 -0
  35. data/spec/performance/support/samples.rb +72 -0
  36. data/test_support/documentation_performance_fixture.rb +106 -0
  37. data/test_support/performance_endpoint_fixture.rb +350 -0
  38. data/test_support/performance_sqlite_fixture.rb +384 -0
  39. metadata +29 -3
@@ -0,0 +1,678 @@
1
+ # All entry points share the calibrated native JSON version. Unbundled
2
+ # workers must activate it before require selects a newer gem in GEM_PATH.
3
+ module HaveAPI
4
+ module Performance
5
+ JSON_VERSION = '2.21.2'.freeze
6
+ gem 'json', "= #{JSON_VERSION}"
7
+ end
8
+ end
9
+ require 'digest'
10
+ require 'json'
11
+ require 'open3'
12
+ require 'rbconfig'
13
+
14
+ module HaveAPI
15
+ module Performance
16
+ REFERENCE = 'b85ec364057f536940d689b09e612512ffffaf82'.freeze
17
+ SAMPLE_SCHEMA = 2
18
+ PROFILES = { 'quick' => { self_pairs: 3, pairs: 5, agreement: 4, target: 1.0, adequate: 0.75, relative: 0.20 },
19
+ 'weekly' => { self_pairs: 3, pairs: 7, agreement: 6, target: 2.0, adequate: 1.5, relative: 0.10 } }.freeze
20
+ MAX_ITERATIONS = 100_000
21
+ REQUEST_LIMIT_SECONDS = 30.0
22
+ SETUP_LIMIT_SECONDS = 120.0
23
+ BENCHMARK_INPUTS = %w[servers/ruby/benchmarks/Gemfile servers/ruby/benchmarks/Gemfile.lock servers/ruby/benchmarks/shell.nix].freeze
24
+
25
+ def self.complex_cases(profile)
26
+ sizes = profile == 'weekly' ? [32] : [16]
27
+ sizes.product(%w[admin member]).map do |resources, user|
28
+ { 'id' => 'docs.html_rust', 'dimensions' => { 'resources' => resources, 'actions' => 4, 'samples' => 2, 'parameters' => 4, 'user' => user } }
29
+ end
30
+ end
31
+ METRICS = %w[wall_seconds cpu_seconds allocated_objects].freeze
32
+ COUNTERS = %w[queries authorizations executions].freeze
33
+ RUNTIME_POLICY = { 'rack_env' => 'test', 'locale' => 'C.UTF-8', 'timezone' => 'UTC', 'yjit' => false, 'rjit' => false }.freeze
34
+ class ContractError < StandardError; end
35
+
36
+ def self.gemspec(root, path)
37
+ script = <<~RUBY
38
+ require 'json'
39
+ spec = Gem::Specification.load(File.basename(ARGV.fetch(0)))
40
+ abort 'Cannot load gemspec' unless spec
41
+ puts JSON.generate(ruby: spec.required_ruby_version.to_s,
42
+ dependencies: spec.dependencies.map { |d| [d.name, d.type.to_s, d.requirement.to_s] }.sort)
43
+ RUBY
44
+ environment = (ENV.keys.grep(/\ABUNDLE/) + %w[RUBYOPT RUBYLIB RUBYGEMS_GEMDEPS]).to_h { |key| [key, nil] }
45
+ output, errors, status = Open3.capture3(environment, RbConfig.ruby, '-e', script, path, chdir: File.dirname(File.join(root, path)))
46
+ raise ContractError, "Cannot load source gemspec #{path}: #{errors.strip}" unless status.success?
47
+
48
+ specification = JSON.parse(output)
49
+ unless Gem::Requirement.new(specification.fetch('ruby')).satisfied_by?(Gem::Version.new(RUBY_VERSION))
50
+ raise ContractError, "Benchmark Ruby does not satisfy #{path}"
51
+ end
52
+
53
+ specification.fetch('dependencies').each do |name, type, requirement|
54
+ next unless type == 'runtime'
55
+ next if %w[haveapi haveapi-client haveapi-rust-client].include?(name)
56
+
57
+ unless Gem::Requirement.new(requirement.split(',').map(&:strip)).satisfied_by?(Gem::Specification.find_by_name(name).version)
58
+ raise ContractError, "Benchmark dependencies do not satisfy #{name} #{requirement}"
59
+ end
60
+ end
61
+ specification
62
+ end
63
+
64
+ # One reviewed maintenance mapping, not a general cherry-pick certificate.
65
+ # Both live entry points verify Git; replay verifies the retained raw delta.
66
+ module Release
67
+ MAPPED = '8b7abc097db49893da9cc8fa4395648901fec71d'.freeze
68
+ METADATA = '1c3c98f0d77e0b23c8da1a8937f25c5799864a3b'.freeze
69
+ DELTA = '0a008acbb46434a0914421064149ce154ea41a6e82a29022c53e666eb2066fd5'.freeze
70
+ DIFF = %w[-c diff.suppressBlankEmpty=false -c core.quotePath=true diff --binary --full-index
71
+ --no-ext-diff --no-textconv --no-color --no-renames --diff-algorithm=myers
72
+ --no-indent-heuristic --unified=3 --inter-hunk-context=0 --src-prefix=a/ --dst-prefix=b/
73
+ --line-prefix= --no-relative -O/dev/null].freeze
74
+ TREES = %w[aaa082cae86f0a24559333213ab959729fb37494 e5a3896cad997648eaa08ff1c1865deca5de9a43
75
+ 32a09190e05a89f6312e07f20a9e4585335e468c d42a0638dc93a40ef08718ae2bf6897df966d980].freeze
76
+ JSON_GEMFILES = %w[Gemfile servers/ruby/Gemfile clients/rust/Gemfile].freeze
77
+ VERSIONS = { 'servers/ruby/lib/haveapi/version.rb' => 'haveapi',
78
+ 'clients/ruby/lib/haveapi/client/version.rb' => 'haveapi-client',
79
+ 'clients/rust/lib/haveapi/rust_client/version.rb' => 'haveapi-rust-client' }.freeze
80
+
81
+ def self.lineage(git:, ancestor:, candidate:)
82
+ # Ordinary master ancestry needs no maintenance objects, including in a
83
+ # single-branch clone. Probe the exceptional mapping only when needed.
84
+ return if ancestor.call(REFERENCE, candidate)
85
+ return unless ancestor.call(MAPPED, candidate)
86
+
87
+ proof = { 'candidate_sha' => candidate, 'reference_sha' => REFERENCE, 'mapped_sha' => MAPPED,
88
+ 'metadata_sha' => METADATA,
89
+ 'trees' => [REFERENCE, MAPPED, "#{METADATA}^", METADATA].map { |sha| git.call('rev-parse', "#{sha}^{tree}").strip },
90
+ 'reference_delta' => git.call(*DIFF, REFERENCE, MAPPED),
91
+ 'metadata_delta' => git.call(*DIFF, "#{METADATA}^", METADATA) }
92
+ validate_lineage(proof, candidate)
93
+ proof
94
+ end
95
+
96
+ def self.validate_lineage(proof, candidate)
97
+ unless proof.fetch('candidate_sha') == candidate && proof.fetch('reference_sha') == REFERENCE &&
98
+ proof.fetch('mapped_sha') == MAPPED && proof.fetch('metadata_sha') == METADATA && proof.fetch('trees') == TREES &&
99
+ proof.fetch('reference_delta') == proof.fetch('metadata_delta') &&
100
+ Digest::SHA256.hexdigest(proof.fetch('reference_delta')) == DELTA
101
+ raise ContractError, 'Invalid published release lineage proof'
102
+ end
103
+ end
104
+
105
+ def self.version(contents)
106
+ value = contents.fetch('VERSION').strip
107
+ unless value.match?(/\A0\.30\.(?:0|[1-9][0-9]*)\z/) && contents.fetch('VERSION') == "#{value}\n"
108
+ raise ContractError, 'Expected own 0.30 patch version'
109
+ end
110
+
111
+ value
112
+ end
113
+
114
+ def self.paths(profile)
115
+ PROFILES.fetch(profile)
116
+ versions = VERSIONS.keys
117
+ %w[VERSION .ruby-version flake.nix flake.lock Gemfile servers/ruby/Gemfile clients/ruby/Gemfile] +
118
+ versions + versions.map { |path| "#{path.split('/lib/').first}/#{VERSIONS.fetch(path)}.gemspec" } +
119
+ ['clients/rust/Gemfile']
120
+ end
121
+
122
+ # Normalize only the exact JSON preparation declaration and validated own
123
+ # VERSION/internal dependency identities. Keep raw and normalized hashes.
124
+ def self.normalize(contents)
125
+ own = version(contents)
126
+ normalized = contents.merge(json_preparation(contents).fetch('normalized_contents'))
127
+ normalized['VERSION'] = "0.30.PATCH\n"
128
+ VERSIONS.each do |path, name|
129
+ next unless contents.has_key?(path)
130
+
131
+ source = contents.fetch(path)
132
+ raise ContractError, "Mismatched local #{name} version" unless source.scan(/^\s*VERSION = '([^']+)'/).flatten == [own]
133
+
134
+ normalized[path] = source.sub(/^(\s*VERSION = )'#{Regexp.escape(own)}'/, "\\1'0.30.PATCH'")
135
+ spec_path = "#{path.split('/lib/').first}/#{name}.gemspec"
136
+ spec = contents.fetch(spec_path)
137
+ raise ContractError, 'Mismatched local package identity' unless spec.match?(/\.name\s*= '#{Regexp.escape(name)}'/)
138
+ next if name == 'haveapi-client'
139
+
140
+ requirement = /(?<=\.add_dependency )'haveapi-client', '~> ([^']+)'/
141
+ raise ContractError, 'Unsynchronized internal client constraint' unless spec.scan(requirement).flatten == [own]
142
+
143
+ normalized[spec_path] = spec.sub(requirement, "'haveapi-client', '~> 0.30.PATCH'")
144
+ end
145
+ normalized
146
+ end
147
+
148
+ def self.capture(contents)
149
+ { 'contents' => contents, 'own_version' => version(contents),
150
+ 'raw_sha256' => contents.transform_values { |value| Digest::SHA256.hexdigest(value) },
151
+ 'normalized_sha256' => normalize(contents).transform_values { |value| Digest::SHA256.hexdigest(value) },
152
+ 'json_preparation' => json_preparation(contents).fetch('declarations') }
153
+ end
154
+
155
+ # A reviewed preparation-only delta: no declaration, the old unversioned
156
+ # declaration, or this exact development pin. Every other byte is retained.
157
+ def self.json_preparation(contents)
158
+ selected = contents.slice(*JSON_GEMFILES)
159
+ raise ContractError, 'Missing JSON preparation Gemfile' unless selected.keys.sort == JSON_GEMFILES.sort
160
+
161
+ declarations = {}
162
+ normalized = selected.to_h do |path, text|
163
+ lines = text.lines.grep(/^\s*gem ['"]json['"]/)
164
+ pin = " gem 'json', '= #{JSON_VERSION}'\n"
165
+ old = " gem 'json'\n"
166
+ unless lines.empty? || lines == [pin] || lines == [old]
167
+ raise ContractError, "Unexpected JSON preparation declaration in #{path}"
168
+ end
169
+
170
+ declarations[path] = if lines.empty?
171
+ 'absent'
172
+ elsif lines == [pin]
173
+ 'exact-development-pin'
174
+ else
175
+ 'old-unversioned'
176
+ end
177
+ [path, lines.empty? ? text : text.sub(lines.first, '')]
178
+ end
179
+ { 'contents' => selected, 'normalized_contents' => normalized,
180
+ 'raw_sha256' => selected.transform_values { |text| Digest::SHA256.hexdigest(text) },
181
+ 'normalized_sha256' => normalized.transform_values { |text| Digest::SHA256.hexdigest(text) },
182
+ 'declarations' => declarations, 'version' => JSON_VERSION }
183
+ end
184
+
185
+ def self.validate_json_preparation(inputs, source)
186
+ receipt = inputs.fetch('json_preparation')
187
+ raise ContractError, 'JSON preparation normalization mismatch' unless receipt == json_preparation(receipt.fetch('contents'))
188
+
189
+ receipt.fetch('raw_sha256').each do |path, digest|
190
+ unless source.fetch('files_sha256').fetch(path) == digest && inputs.fetch('manifests').fetch(path) == receipt.fetch('normalized_sha256').fetch(path)
191
+ raise ContractError, 'JSON preparation escaped verified source'
192
+ end
193
+ end
194
+ end
195
+
196
+ def self.validate_inputs(inputs, source, profile)
197
+ receipt = inputs.fetch('release_versions')
198
+ raise ContractError, 'Incomplete release input graph' unless receipt.fetch('contents').keys.sort == paths(profile).sort
199
+ raise ContractError, 'Release input normalization mismatch' unless receipt == capture(receipt.fetch('contents'))
200
+
201
+ expected = receipt.fetch('normalized_sha256')
202
+ expected = expected.except('flake.nix', 'flake.lock') if inputs.has_key?('benchmark')
203
+ raise ContractError, 'Release normalized graph mismatch' unless inputs.fetch('manifests') == expected
204
+
205
+ receipt.fetch('raw_sha256').each do |path, digest|
206
+ raise ContractError, 'Release input escaped verified source' unless source.fetch('files_sha256').fetch(path) == digest
207
+ end
208
+ receipt.fetch('own_version')
209
+ end
210
+
211
+ def self.graph(inputs)
212
+ inputs.except('release_versions', 'json_preparation', 'contributor_flakes')
213
+ end
214
+
215
+ def self.html(body, own)
216
+ raise ContractError, 'Expected own 0.30 patch version' unless own.match?(/\A0\.30\.(?:0|[1-9][0-9]*)\z/)
217
+
218
+ # Only the exact framework footer and emitted WebUI hrefs may change.
219
+ # Opaque content and complete quoted tag attributes remain byte-exact.
220
+ footer = '<p class="text-right">Generated by <a href="https://github.com/vpsfreecz/haveapi">HaveAPI</a> v'
221
+ pattern = %r{<!--.*?-->|(?i:<(?<opaque>script|style|pre|code|textarea)\b[^>]*>.*?</\k<opaque>\s*>)|(?<footer>#{Regexp.escape(footer)})(?<footer_version>[^<]*)</p>|(?<link><a\s+href=")https://webui\.haveapi\.org/v#{Regexp.escape(own)}/(?=#[^"<>]*")|<(?:"[^"]*"|'[^']*'|[^'">])*>}m
222
+ footers = 0
223
+ normalized = body.gsub(pattern) do |match|
224
+ if Regexp.last_match[:footer]
225
+ footers += 1
226
+ raise ContractError, 'Documentation footer differs from own source VERSION' unless Regexp.last_match[:footer_version] == own
227
+
228
+ "#{Regexp.last_match[:footer]}0.30.PATCH</p>"
229
+ elsif Regexp.last_match[:link]
230
+ "#{Regexp.last_match[:link]}https://webui.haveapi.org/v0.30.PATCH/"
231
+ else
232
+ match
233
+ end
234
+ end
235
+ raise ContractError, 'Expected one framework documentation footer' unless footers == 1
236
+
237
+ normalized
238
+ end
239
+ end
240
+
241
+ def self.validate_source_pair(profile:, baseline_sha:, candidate_sha:)
242
+ raise ContractError, 'Baseline and candidate must differ' if baseline_sha == candidate_sha
243
+ return unless profile == 'weekly' && baseline_sha != REFERENCE
244
+
245
+ raise ContractError, 'Weekly baseline must be the fixed reference'
246
+ end
247
+
248
+ def self.push_pair!(selection, baseline:, candidate:)
249
+ return false unless selection['push_pair']
250
+
251
+ expected = { 'event_name' => 'push', 'before' => baseline, 'after' => candidate }
252
+ unless selection['event_name'] == 'push' && selection['profile'] == 'quick' && selection['push_pair'] == expected &&
253
+ selection['baseline_sha'] == baseline && selection['candidate_sha'] == candidate && baseline != candidate &&
254
+ [baseline, candidate].all? { |sha| sha.is_a?(String) && sha.match?(/\A[0-9a-f]{40}\z/) && sha != '0' * 40 }
255
+ raise ContractError, 'Invalid quick push event pair'
256
+ end
257
+
258
+ true
259
+ end
260
+
261
+ # Canonicalization ignores map order only. Arrays and scalar types retain
262
+ # their meaning; duplicate JSON or symbol/string-normalized keys fail closed.
263
+ module Identity
264
+ # A sample owns this snapshot; later response/fixture mutations cannot
265
+ # change the body proof retained for another operation in that sample.
266
+ def self.snapshot(value)
267
+ case value
268
+ when Hash then value.to_h { |key, item| [snapshot(key), snapshot(item)] }.freeze
269
+ when Array then value.map { |item| snapshot(item) }.freeze
270
+ when String then value.dup.freeze
271
+ else value.freeze
272
+ end
273
+ end
274
+
275
+ def self.canonical(value)
276
+ case value
277
+ when Hash
278
+ normalized = {}
279
+ value.each do |key, item|
280
+ raise ContractError, 'Non-string JSON map key' unless key.is_a?(String) || key.is_a?(Symbol)
281
+ raise ContractError, "Duplicate normalized key #{key}" if normalized.has_key?(key.to_s)
282
+
283
+ normalized[key.to_s] = canonical(item)
284
+ end
285
+ normalized.sort.to_h
286
+ when Array then value.map { |item| canonical(item) }
287
+ when String, Integer, TrueClass, FalseClass, NilClass then value
288
+ when Float
289
+ raise ContractError, 'Non-finite JSON number' unless value.finite?
290
+
291
+ value
292
+ else raise ContractError, "Non-JSON value #{value.class}"
293
+ end
294
+ end
295
+
296
+ def self.digest(value)
297
+ Digest::SHA256.hexdigest(JSON.generate(canonical(value)))
298
+ end
299
+
300
+ def self.json(response)
301
+ body = parse(response.body)
302
+ canonical = canonical(body)
303
+ { http_status: response.status, envelope_status: body.fetch('status'),
304
+ body_bytes: response.body.bytesize, body_sha256: Digest::SHA256.hexdigest(response.body),
305
+ canonical_sha256: Digest::SHA256.hexdigest(JSON.generate(canonical)), canonical_body: canonical }
306
+ end
307
+
308
+ def self.parse(string)
309
+ JSON.parse(string, allow_duplicate_key: false)
310
+ rescue JSON::ParserError => e
311
+ raise ContractError, "Duplicate key or invalid JSON: #{e.message}"
312
+ end
313
+
314
+ def self.key(spec)
315
+ JSON.generate(canonical('id' => spec.fetch('id'), 'dimensions' => spec.fetch('dimensions')))
316
+ end
317
+ end
318
+
319
+ class Comparator
320
+ POLICY = 'per-revision-response-stability-v1'.freeze
321
+ SOURCE_SHA256 = Digest::SHA256.file(__FILE__).hexdigest.freeze
322
+
323
+ def initialize(samples, budgets, budget_sha256:)
324
+ @samples = samples
325
+ @budgets = budgets
326
+ @budget_sha256 = budget_sha256
327
+ end
328
+
329
+ def compare
330
+ validate_header
331
+ inventory = @samples.fetch('inventory').map { |spec| Identity.key(spec) }
332
+ cases = @samples.fetch('cases')
333
+ budget_cases = @budgets.fetch('profiles').fetch(@samples.fetch('profile'))
334
+ unless inventory.uniq == inventory && inventory.sort == cases.map { |item| Identity.key(item) }.sort &&
335
+ inventory.sort == (budget_cases + Performance.complex_cases(@samples.fetch('profile'))).map { |item| Identity.key(item) }.sort
336
+ raise ContractError, 'Missing, duplicate or unexpected workload'
337
+ end
338
+
339
+ results = cases.map do |item|
340
+ budget = budget_cases.find { |entry| Identity.key(entry) == Identity.key(item) }
341
+ compare_case(item, budget)
342
+ end
343
+ status = if results.any? { |item| item['classification'] == 'regression' }
344
+ 'regression'
345
+ elsif results.any? { |item| item['classification'] == 'inconclusive' }
346
+ 'inconclusive'
347
+ else
348
+ 'pass'
349
+ end
350
+ @samples.slice('schema_version', 'profile', 'baseline_sha', 'candidate_sha', 'harness_sha256', 'budget_sha256')
351
+ .merge('status' => status, 'cases' => results, 'calibration_environment' => 'weekly')
352
+ rescue KeyError, TypeError, NoMethodError, ArgumentError => e
353
+ raise ContractError, "Invalid comparison input: #{e.message}"
354
+ end
355
+
356
+ private
357
+
358
+ def validate_header
359
+ raise ContractError, "Unsupported samples schema; expected #{SAMPLE_SCHEMA}" unless @samples.fetch('schema_version').eql?(SAMPLE_SCHEMA)
360
+ raise ContractError, 'Unsupported budget schema' unless @budgets.fetch('schema_version') == 1
361
+ raise ContractError, 'Wrong fixed reference' unless @budgets.fetch('reference_sha') == REFERENCE
362
+ raise ContractError, 'Budget digest mismatch' unless @samples.fetch('budget_sha256') == @budget_sha256
363
+
364
+ @policy = PROFILES.fetch(@samples.fetch('profile'))
365
+ calibration = @budgets.fetch('calibration').fetch(@samples.fetch('profile'))
366
+ unless calibration.fetch('source_sha') == REFERENCE && calibration.fetch('raw_sha256').match?(/\A[0-9a-f]{64}\z/) &&
367
+ calibration.fetch('warmup_batches') == 2 && calibration.fetch('iterations').is_a?(Integer) && calibration['iterations'] > 0 &&
368
+ !calibration.fetch('fixture_sha256').empty? && !calibration.fetch('inputs').empty?
369
+ raise ContractError, 'Missing calibrated reference evidence'
370
+ end
371
+
372
+ %w[baseline_sha candidate_sha harness_sha256].each do |field|
373
+ length = field.end_with?('_sha256') ? 64 : 40
374
+ raise ContractError, "Missing #{field}" unless @samples.fetch(field).match?(/\A[0-9a-f]{#{length}}\z/)
375
+ end
376
+ Performance.validate_source_pair(profile: @samples.fetch('profile'), baseline_sha: @samples.fetch('baseline_sha'),
377
+ candidate_sha: @samples.fetch('candidate_sha'))
378
+ raise ContractError, 'Missing fixture provenance' if @samples.fetch('fixture_sha256').empty?
379
+ raise ContractError, 'Missing source provenance' unless @samples.fetch('sources').keys.sort == %w[baseline candidate]
380
+ raise ContractError, 'Missing environment' if @samples.fetch('environment').empty?
381
+ unless @samples.fetch('environment') == @budgets.fetch('environment').fetch('weekly')
382
+ raise ContractError, 'Calibration environment mismatch'
383
+ end
384
+
385
+ sources = @samples.fetch('sources')
386
+ lineage = @samples['release_lineage']
387
+ Release.validate_lineage(lineage, @samples.fetch('candidate_sha')) if lineage
388
+ %w[baseline candidate].each do |role|
389
+ source = sources.fetch(role)
390
+ unless source.fetch('sha') == @samples.fetch("#{role}_sha") && source.fetch('root').start_with?('/') &&
391
+ source.fetch('tree').match?(/\A[0-9a-f]{40}\z/) && source.fetch('tracked_files').is_a?(Integer) && source['tracked_files'] > 0
392
+ raise ContractError, 'Invalid source proof'
393
+ end
394
+ end
395
+ observations = @samples.fetch('observations')
396
+ raise ContractError, 'Missing worker provenance' if observations.empty?
397
+
398
+ observations.each_value do |observation|
399
+ source = observation.fetch('source')
400
+ raise ContractError, 'Worker root/SHA absent from source proof' unless sources.values.any? { |entry| entry['root'] == source['root'] && entry['sha'] == source['sha'] }
401
+ raise ContractError, 'Missing loaded sources' if source.fetch('loaded_sources').empty?
402
+ raise ContractError, 'Runtime policy mismatch' unless observation.fetch('runtime_policy') == RUNTIME_POLICY
403
+ raise ContractError, 'Missing host diagnostics' unless %w[hostname os cpu].all? { |key| observation.fetch('host').fetch(key).is_a?(String) }
404
+
405
+ selected = sources.values.find { |entry| entry['root'] == source['root'] && entry['sha'] == source['sha'] }
406
+ source.fetch('loaded_sources').each do |path, sha256|
407
+ raise ContractError, 'Loaded runtime not in verified source tree' unless selected.fetch('files_sha256').fetch(path) == sha256
408
+ end
409
+ inputs = observation.fetch('inputs')
410
+ if inputs.has_key?('benchmark')
411
+ tool = inputs.fetch('benchmark')
412
+ raise ContractError, 'Incomplete benchmark inputs' unless tool.keys.sort == BENCHMARK_INPUTS.sort
413
+
414
+ tool.each do |path, input|
415
+ unless Digest::SHA256.hexdigest(input.fetch('content')) == input.fetch('sha256') &&
416
+ @samples.fetch('inputs').fetch('benchmark').fetch(path) == input
417
+ raise ContractError, 'Benchmark input digest mismatch'
418
+ end
419
+ end
420
+ unless inputs.fetch('locks') == { 'servers/ruby/benchmarks/Gemfile.lock' => tool.fetch('servers/ruby/benchmarks/Gemfile.lock').fetch('sha256') }
421
+ raise ContractError, 'Benchmark lock identity mismatch'
422
+ end
423
+
424
+ inputs.fetch('contributor_flakes').each do |path, input|
425
+ unless %w[flake.nix flake.lock].include?(path) && Digest::SHA256.hexdigest(input.fetch('content')) == selected.fetch('files_sha256').fetch(path)
426
+ raise ContractError, 'Contributor flake escaped source'
427
+ end
428
+ end
429
+ unless inputs.fetch('contributor_flakes').keys.sort == %w[flake.lock flake.nix]
430
+ raise ContractError, 'Missing contributor flake provenance'
431
+ end
432
+
433
+ inputs.fetch('manifests').each do |path, digest|
434
+ next if Release::JSON_GEMFILES.include?(path) || lineage
435
+
436
+ raise ContractError, 'Dependency manifest escaped source' unless selected.fetch('files_sha256').fetch(path) == digest
437
+ end
438
+ elsif inputs.has_key?('contributor_flakes')
439
+ raise ContractError, 'Contributor flake separation without benchmark inputs'
440
+ end
441
+ if lineage
442
+ Release.validate_inputs(inputs, selected, @samples.fetch('profile'))
443
+ elsif inputs.has_key?('release_versions')
444
+ raise ContractError, 'Unproven own-version normalization'
445
+ else
446
+ Release.validate_json_preparation(inputs, selected)
447
+ end
448
+ unless observation.fetch('environment') == @samples['environment'] && Release.graph(inputs) == @samples.fetch('inputs') &&
449
+ observation.fetch('fixture_sha256') == @samples['fixture_sha256']
450
+ raise ContractError, 'Worker provenance mismatch'
451
+ end
452
+ end
453
+ return if @samples.fetch('inputs').fetch('locks').any? && @samples['inputs'].fetch('manifests').any?
454
+
455
+ raise ContractError, 'Missing dependency graph'
456
+ end
457
+
458
+ def number(value)
459
+ raise ContractError, 'Invalid nonnegative finite metric' unless value.is_a?(Numeric) && value.finite? && value >= 0
460
+
461
+ value
462
+ end
463
+
464
+ def median(values)
465
+ sorted = values.sort
466
+ middle = sorted.length / 2
467
+ sorted.length.odd? ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2.0
468
+ end
469
+
470
+ def validate_sample(sample, iterations, budget, spec)
471
+ workload = spec.fetch('id')
472
+ METRICS.each { |metric| number(sample.fetch(metric)) }
473
+ if sample.fetch('wall_seconds') * iterations > REQUEST_LIMIT_SECONDS
474
+ raise ContractError, "Request batch exceeds #{REQUEST_LIMIT_SECONDS}s: #{workload}"
475
+ end
476
+ raise ContractError, 'Operation count mismatch' unless sample.fetch('operations').is_a?(Integer) && sample.fetch('operations') == iterations
477
+
478
+ outputs = sample.fetch('outputs')
479
+ counts = outputs.map do |output|
480
+ count = output.fetch('count')
481
+ raise ContractError, 'Invalid response occurrence count' unless count.is_a?(Integer) && count.between?(1, iterations)
482
+
483
+ count
484
+ end
485
+ raise ContractError, 'Response occurrence count mismatch' unless counts.sum == iterations
486
+
487
+ counters = sample.fetch('counters')
488
+ unless counters.fetch('operations').is_a?(Integer) && counters.fetch('operations') == iterations
489
+ raise ContractError, 'Validated counter operation count mismatch'
490
+ end
491
+
492
+ proof = @samples.fetch('observations').fetch(sample.fetch('observation'))
493
+ raise ContractError, 'Sample source provenance missing' unless proof
494
+
495
+ bodies = sample.fetch('bodies')
496
+ unless bodies.keys.sort == outputs.map { |output| output.fetch('body_sha256') }.sort
497
+ raise ContractError, 'Missing, duplicate or unreferenced response example'
498
+ end
499
+
500
+ body_proofs = bodies.to_h do |digest, value|
501
+ raise ContractError, 'Invalid raw output digest' unless digest.match?(/\A[0-9a-f]{64}\z/)
502
+ raise ContractError, 'Raw output digest mismatch' unless Digest::SHA256.hexdigest(value.fetch('body')) == digest
503
+
504
+ canonical = value['canonical_body']
505
+ canonical_digest = canonical.nil? ? digest : Identity.digest(canonical)
506
+ if !canonical.nil? && Identity.digest(Identity.parse(value.fetch('body'))) != canonical_digest
507
+ raise ContractError, 'Raw/canonical output values differ'
508
+ end
509
+
510
+ envelope = canonical.nil? || workload.start_with?('rust.') ? nil : canonical.fetch('status')
511
+ [digest, { body: value.fetch('body').dup.freeze, bytes: value.fetch('body').bytesize, canonical_sha256: canonical_digest.dup.freeze,
512
+ canonical_nil: canonical.nil?, envelope: Identity.snapshot(envelope) }.freeze]
513
+ end
514
+ outputs.each do |output|
515
+ raise ContractError, 'Invalid output status' unless output.fetch('http_status') == 200
516
+ raise ContractError, 'Missing envelope status' unless output.has_key?('envelope_status')
517
+ raise ContractError, 'Empty output' unless number(output.fetch('body_bytes')) > 0
518
+
519
+ %w[body_sha256 canonical_sha256].each do |field|
520
+ raise ContractError, "Missing #{field}" unless output.fetch(field).match?(/\A[0-9a-f]{64}\z/)
521
+ end
522
+ body_digest = output.fetch('body_sha256')
523
+ body_proof = body_proofs.fetch(body_digest)
524
+ raise ContractError, 'Raw body byte count mismatch' unless body_proof.fetch(:bytes) == output['body_bytes']
525
+ raise ContractError, 'Raw body envelope status mismatch' unless body_proof.fetch(:envelope) == output.fetch('envelope_status')
526
+
527
+ comparison = output['comparison']
528
+ if output.has_key?('comparison') && !(@samples['release_lineage'] && %w[docs.html_resources docs.html_actions docs.html_rust].include?(workload) && body_proof.fetch(:canonical_nil) &&
529
+ comparison == { 'kind' => 'own-webui-href', 'own_version' => proof.fetch('inputs').fetch('release_versions').fetch('own_version') })
530
+ raise ContractError, 'Invalid own-version HTML comparison'
531
+ end
532
+
533
+ digest = if comparison
534
+ Digest::SHA256.hexdigest(Release.html(body_proof.fetch(:body), comparison.fetch('own_version')))
535
+ else
536
+ body_proof.fetch(:canonical_sha256)
537
+ end
538
+ if digest != output['canonical_sha256']
539
+ raise ContractError, 'Canonical output digest mismatch'
540
+ end
541
+ end
542
+ expected = budget ? budget.fetch('counters') : { 'queries' => 0, 'authorizations' => (spec.fetch('dimensions').fetch('resources') * spec.fetch('dimensions').fetch('actions')) + 1, 'executions' => 0 }
543
+ raise ContractError, 'Missing complex documentation authorization' if !budget && expected['authorizations'] <= 0
544
+ raise ContractError, 'Uncalibrated counter budget' unless (COUNTERS - expected.keys).empty?
545
+ unless expected.values.all? { |value| value.is_a?(Integer) && value >= 0 }
546
+ raise ContractError, 'Invalid calibrated counter budget'
547
+ end
548
+
549
+ record = counters.fetch('record')
550
+ raise ContractError, 'Counter keys mismatch' unless record.keys.sort == expected.keys.sort
551
+
552
+ record.each do |key, value|
553
+ raise ContractError, 'Invalid counter' unless value.is_a?(Integer) && value >= 0
554
+ raise ContractError, "Counter contract #{key}: #{value}, expected #{expected[key]}" unless value == expected[key]
555
+ end
556
+ end
557
+
558
+ def output_counts(outputs, digest = 'canonical_sha256')
559
+ outputs.each_with_object(Hash.new(0)) do |output, counts|
560
+ identity = JSON.generate(output.values_at('http_status', 'envelope_status', digest))
561
+ counts[identity] += output.fetch('count')
562
+ end
563
+ end
564
+
565
+ def same_outputs(left, right)
566
+ output_counts(left) == output_counts(right)
567
+ end
568
+
569
+ def response_changes(pairs)
570
+ outputs = %w[baseline candidate].to_h do |role|
571
+ [role, pairs.flat_map { |pair| pair.fetch(role).fetch('outputs') }]
572
+ end
573
+ bytes = outputs.transform_values do |examples|
574
+ examples.sum { |output| output.fetch('body_bytes') * output.fetch('count') }.to_f / examples.sum { |output| output.fetch('count') }
575
+ end
576
+ delta = bytes.fetch('candidate') - bytes.fetch('baseline')
577
+ { 'raw_changed' => output_counts(outputs.fetch('baseline'), 'body_sha256') != output_counts(outputs.fetch('candidate'), 'body_sha256'),
578
+ 'canonical_changed' => !same_outputs(outputs.fetch('baseline'), outputs.fetch('candidate')),
579
+ 'bytes_per_request' => bytes.merge('delta' => delta, 'delta_percent' => 100.0 * delta / bytes.fetch('baseline')) }
580
+ end
581
+
582
+ def compare_case(item, budget)
583
+ iterations = item.fetch('iterations')
584
+ raise ContractError, 'Invalid batch size' unless iterations.is_a?(Integer) && iterations.between?(1, MAX_ITERATIONS)
585
+
586
+ self_pairs = item.fetch('self_pairs')
587
+ pairs = item.fetch('pairs')
588
+ raise ContractError, 'Incomplete pair inventory' unless self_pairs.length == @policy[:self_pairs] && pairs.length == @policy[:pairs]
589
+
590
+ ceiling = nil
591
+ if budget
592
+ reference = number(budget.fetch('reference_allocations'))
593
+ ceiling = number(budget.fetch('allocation_ceiling'))
594
+ raise ContractError, 'Uncalibrated allocation ceiling' unless reference > 0 && ceiling == (reference * 1.25).ceil + 32
595
+ end
596
+
597
+ outputs_by_source = {}
598
+ canonical_counters = nil
599
+ (self_pairs + pairs).each_with_index do |pair, index|
600
+ expected_order = if index < self_pairs.length
601
+ %w[baseline baseline]
602
+ elsif (index - self_pairs.length).even?
603
+ %w[baseline candidate]
604
+ else
605
+ %w[candidate baseline]
606
+ end
607
+ raise ContractError, 'Wrong comparison order' unless pair.fetch('order') == expected_order
608
+
609
+ pair_index = index < self_pairs.length ? index : index - self_pairs.length
610
+ expected_workload_order = pair_index.even? ? 'forward' : 'reverse'
611
+ raise ContractError, 'Wrong workload order' unless pair.fetch('workload_order') == expected_workload_order
612
+
613
+ %w[baseline candidate].each do |role|
614
+ sample = pair.fetch(role)
615
+ validate_sample(sample, iterations, budget, item)
616
+ unless budget
617
+ canonical_counters ||= sample.fetch('counters')
618
+ unless sample.fetch('counters') == canonical_counters
619
+ raise ContractError, 'Complex documentation authorization counts differ'
620
+ end
621
+ end
622
+ expected_sha = index < self_pairs.length ? @samples['baseline_sha'] : @samples["#{role}_sha"]
623
+ actual_sha = @samples['observations'].fetch(sample['observation']).fetch('source').fetch('sha')
624
+ raise ContractError, 'Sample belongs to wrong revision' unless actual_sha == expected_sha
625
+ if @samples['observations'].fetch(sample['observation']).fetch('workload_order') != expected_workload_order
626
+ raise ContractError, 'Worker workload order mismatch'
627
+ end
628
+
629
+ outputs_by_source[actual_sha] ||= sample.fetch('outputs')
630
+ unless same_outputs(outputs_by_source.fetch(actual_sha), sample.fetch('outputs'))
631
+ raise ContractError, "Response identity differs within revision #{actual_sha}"
632
+ end
633
+ end
634
+ end
635
+ classifications = %w[wall_seconds cpu_seconds].to_h do |metric|
636
+ baseline = pairs.map { |pair| pair['baseline'][metric] }
637
+ noise = [self_pairs.map { |pair| (pair['baseline'][metric] - pair['candidate'][metric]).abs }.max,
638
+ 0.00005 / iterations].max
639
+ excess = pairs.map do |pair|
640
+ b = pair['baseline'][metric]
641
+ pair['candidate'][metric] - b - [@policy[:relative] * b, 0.050 / iterations].max
642
+ end
643
+ failed = excess.count { |value| value > noise }
644
+ passed = excess.count { |value| value < -noise }
645
+ signal = median(baseline) * iterations >= @policy[:adequate]
646
+ classification = 'inconclusive'
647
+ if signal
648
+ classification = 'regression' if failed >= @policy[:agreement]
649
+ classification = 'pass' if passed >= @policy[:agreement]
650
+ end
651
+ [metric, { 'classification' => classification, 'noise' => noise,
652
+ 'baseline_batch_seconds' => median(baseline) * iterations, 'adequate_signal' => signal,
653
+ 'relative_budget' => @policy[:relative], 'minimum_batch_increase_seconds' => 0.050,
654
+ 'excess' => excess, 'regression_agreement' => failed, 'pass_agreement' => passed }]
655
+ end
656
+ base_allocations = median(pairs.map { |pair| pair['baseline']['allocated_objects'] })
657
+ candidate_allocations = median(pairs.map { |pair| pair['candidate']['allocated_objects'] })
658
+ allocations_failed = candidate_allocations > base_allocations + [0.10 * base_allocations, 32].max || (ceiling && candidate_allocations > ceiling)
659
+ classification = if allocations_failed || classifications.values.any? { |result| result['classification'] == 'regression' }
660
+ 'regression'
661
+ elsif classifications.values.any? { |result| result['classification'] == 'inconclusive' }
662
+ 'inconclusive'
663
+ else
664
+ 'pass'
665
+ end
666
+ item.slice('id', 'dimensions', 'iterations')
667
+ .merge('classification' => classification, 'timing' => classifications, 'responses' => response_changes(pairs),
668
+ 'correctness' => { 'status' => 'pass', 'validated_samples' => 2 * (self_pairs.length + pairs.length),
669
+ 'operations_per_sample' => iterations, 'distinct_responses' => outputs_by_source.fetch(@samples.fetch('baseline_sha')).length,
670
+ 'distinct_responses_by_source' => outputs_by_source.transform_values(&:length) },
671
+ 'pilot' => item.fetch('pilot', {}).slice('selected_iterations', 'warmup_iterations', 'target_seconds',
672
+ 'baseline_batch_seconds', 'adequate_signal', 'capped', 'calibration_adjusted'),
673
+ 'allocations' => { 'baseline_median' => base_allocations, 'candidate_median' => candidate_allocations,
674
+ 'reference_ceiling' => ceiling, 'calibrated' => !budget.nil?, 'classification' => allocations_failed ? 'regression' : 'pass' })
675
+ end
676
+ end
677
+ end
678
+ end