haveapi 0.30.0 → 0.30.2

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