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,21 @@
1
+ let
2
+ pkgs = import (builtins.fetchTree {
3
+ type = "github";
4
+ owner = "NixOS";
5
+ repo = "nixpkgs";
6
+ rev = "02c087f3fe673c9e317f206a627cb6b910336dbe";
7
+ narHash = "sha256-IoNisKcGbUjdd45qCqywGMCYmZ5EnkxezvLII0FWwNA=";
8
+ }) { };
9
+ in
10
+ pkgs.mkShell {
11
+ name = "haveapi-benchmarks";
12
+ packages = with pkgs; [ ruby_3_4 git gnumake pkg-config openssl libffi ];
13
+ LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ pkgs.stdenv.cc.cc.lib ];
14
+ shellHook = ''
15
+ for name in ''${!BUNDLE@}; do unset "$name"; done
16
+ unset RUBYOPT RUBYLIB RUBYGEMS_GEMDEPS
17
+ export GEM_HOME="''${HAVEAPI_BENCHMARK_GEM_HOME:-${toString ./.}/.gems}"
18
+ export GEM_PATH="$GEM_HOME:${pkgs.ruby_3_4}/lib/ruby/gems/3.4.0"
19
+ export PATH="$GEM_HOME/bin:$PATH"
20
+ '';
21
+ }
@@ -2,20 +2,23 @@
2
2
 
3
3
  The Ruby server renders examples from the current user's API description.
4
4
  JavaScript and TypeScript examples cover callback and async clients; Python
5
- examples cover sync and async clients. These clients need no server configuration.
5
+ examples cover sync and async clients. These clients need no server
6
+ configuration.
6
7
  Ruby, PHP, CLI, curl and HTTP examples are also available. A sample
7
8
  that a client cannot represent shows a limitation instead of dropping input or
8
9
  inventing a successful result.
9
10
 
10
- Go and Rust tabs require a profile for a generated SDK. Their package versions
11
- are independent of HaveAPI and the API version. Without a profile, the server
11
+ Go, Rust and generated TypeScript tabs require a profile for a generated SDK.
12
+ Their package versions are independent of HaveAPI and the API version. Without a
13
+ profile, the server
12
14
  hides those clients from the overview and all initialization, authentication and
13
15
  action panels. Install the matching generator gem only when configuring its
14
- profile; ordinary servers do not depend on either generator.
16
+ profile; ordinary servers do not depend on these generators.
15
17
 
16
18
  ## Configure a generated SDK
17
19
 
18
- Register profiles before mounting the server. For an SDK that matches the current
20
+ Register profiles before mounting the server. For an SDK that matches the
21
+ current
19
22
  API, configure its package identity:
20
23
 
21
24
  ```ruby
@@ -25,25 +28,34 @@ api.documentation_clients('1.0',
25
28
  },
26
29
  rust: {
27
30
  crate_name: 'widget-client'
31
+ },
32
+ typescript: {
33
+ package_name: '@example/widget-client'
28
34
  }
29
35
  )
30
36
  api.mount('/api/')
31
37
  ```
32
38
 
33
39
  `generator_version` defaults to the server's `HaveAPI::VERSION`. Install the
34
- optional `haveapi-go-client` or `haveapi-rust-client` generator gem at that
35
- version. An explicit `generator_version` requires the installed generator to
36
- report that version; it never downloads one. It is independent of the SDK and
37
- API versions.
38
-
39
- At the end of mount, the server builds one private structural model per configured
40
- API version. The model includes authentication resources, routes and changes from
40
+ matching generator in the server's Ruby environment: `haveapi-go-client`,
41
+ `haveapi-rust-client`, or the source-distributed `haveapi-typescript-client`.
42
+ See the [TypeScript generator guide](../clients/typescript/README.md) for its
43
+ setup. An explicit `generator_version` requires the installed generator to
44
+ report
45
+ that version; the server never downloads one. This version is independent of the
46
+ SDK and API versions.
47
+
48
+ At the end of mount, the server builds one private structural model per
49
+ configured
50
+ API version. The model includes authentication resources, routes and changes
51
+ from
41
52
  post-mount hooks, so canonical names and parent arguments use the complete API
42
53
  declaration. The private copy excludes examples, prose and parameter defaults.
43
54
  HTML requests use the current user's authorized description, fields and samples;
44
55
  they never expose the private model or its initialization diagnostics. Without
45
- a configured profile, the server loads no optional generator and shows no Go or
46
- Rust panels.
56
+ a configured profile, the server loads no optional generator and hides the Go,
57
+ Rust and generated TypeScript panels. The callback and async TypeScript
58
+ examples remain separate and need no profile.
47
59
 
48
60
  Automatic profiles assume that the installed SDK matches the current server's
49
61
  generation contract. A package name or version does not prove its contents.
@@ -53,8 +65,9 @@ schema override below when the SDK represents an older or restricted API.
53
65
 
54
66
  ### Optional SDK versions and sources
55
67
 
56
- Set `module_version: 'v1.2.3'` for Go or `crate_version: '1.2.3'` for Rust when a
57
- specific package version is needed. Omit the field or use nil to let the user's
68
+ Set `module_version: 'v1.2.3'` for Go, `crate_version: '1.2.3'` for Rust or
69
+ `package_version: '1.2.3'` for TypeScript when a specific package version is
70
+ needed. Omit the field or use nil to let the user's
58
71
  package manager select a release. The server does not query a registry or claim
59
72
  to have resolved a version. Omitted versions produce these installation hints:
60
73
 
@@ -63,15 +76,25 @@ go get example.org/widget/client@latest
63
76
  cargo add widget-client
64
77
  # Blocking Rust variant:
65
78
  cargo add widget-client --features blocking
79
+ npm install @example/widget-client haveapi-client@0.30.2
66
80
  ```
67
81
 
68
82
  Go resolves `@latest` by its normal rules. Cargo selects a compatible release
69
83
  according to its resolver, manifest and toolchain; existing lockfiles still
70
84
  apply. Explicit Cargo versions remain version requirements, without an added
71
- exact `=` pin. Latest selection can change over time; supply a version to control
85
+ exact `=` pin. Latest selection can change over time; supply a version to
86
+ control
72
87
  the selection. Cargo never receives a literal `version = "latest"`. Rust
73
88
  examples also show their Tokio and serde_json requirements.
74
89
 
90
+ TypeScript accepts ordinary lowercase npm package names, including scoped names,
91
+ and optional concrete SemVer versions. Tags, version ranges, Git URLs, local
92
+ paths
93
+ and custom factories are not profile options. Configure a package that users can
94
+ install; the server does not check its availability or publish it. Generated
95
+ snippets use the package's public `createClient` export and the published
96
+ `haveapi-client` 0.30.2 runtime.
97
+
75
98
  For an unpublished Rust SDK, supply an HTTPS Git URL and a full commit SHA:
76
99
 
77
100
  ```ruby
@@ -118,13 +141,20 @@ api.documentation_clients('1.0',
118
141
  snapshot_path: '/srv/widget-sdk/api-1.0.snapshot.json',
119
142
  generator_config_path: '/srv/widget-sdk/generator.toml',
120
143
  generator_version: '0.30.0'
144
+ },
145
+ typescript: {
146
+ package_name: '@example/widget-client',
147
+ package_version: '1.2.3',
148
+ snapshot_path: '/srv/widget-sdk/api-1.0.snapshot.json',
149
+ generator_version: '0.30.0'
121
150
  }
122
151
  )
123
152
  ```
124
153
 
125
154
  Paths name trusted absolute local files. `description_path` is Go's saved
126
- machine-readable description, not HTML. `snapshot_path` is Rust's saved schema
127
- and capture metadata. They select an exact SDK shape in place of the current
155
+ machine-readable description, not HTML. For Rust and TypeScript, `snapshot_path`
156
+ selects the saved schema and capture metadata. These files select an exact SDK
157
+ shape in place of the current
128
158
  server declaration. Keep the files private and retain them with the SDK release.
129
159
 
130
160
  Rust's legacy `generator_config_path` supplies the crate name/version and full
@@ -138,7 +168,8 @@ Saved files are read and validated during registration. Automatic models are
138
168
  initialized at the end of mount. Missing files or generators, actual generator
139
169
  version mismatches, unknown fields, conflicting identities and invalid schemas
140
170
  fail startup. Mounting also rejects unavailable API versions and mismatched Rust
141
- snapshot versions. Failed mounting publishes no profiles and leaves no usable
171
+ and TypeScript snapshot versions. Failed mounting publishes no profiles and
172
+ leaves no usable
142
173
  server application. An older explicitly pinned generator can serve a suitable
143
174
  legacy file profile, but automatic/inline Rust mode requires the new factory
144
175
  interface and never guesses a fallback.
@@ -161,8 +192,21 @@ Existing renderer positional constructors and `init`, `auth(method, desc)` and
161
192
  The registry binds a request-local `HaveAPI::ClientExample::Context` through
162
193
  `with_context`. It holds the selected version, URLs including the API mount,
163
194
  the filtered description, and the current resource, action and sample.
164
- `context.with(...)` creates another frozen copy without freezing the server's
165
- shared description.
195
+ Construction copies and freezes Hash, Array and String data without freezing
196
+ the caller's containers. Each request owns its description snapshot.
197
+ `context.with(...)` creates a distinct frozen context, sharing unchanged fields
198
+ by identity and normalizing only explicitly supplied fields. A shallow-frozen
199
+ input container still needs copying; explicitly supplying an existing owned
200
+ Hash,
201
+ Array or String also copies it. Renderers must treat context-owned data as
202
+ read-only.
203
+
204
+ The profile is a shared reference; registered profiles and their adapters are
205
+ frozen separately. Other object types retain their existing reference semantics,
206
+ so context construction is not an arbitrary-object deep-freezing contract.
207
+ Subclasses of `Context` continue deriving through their constructors, preserving
208
+ custom initialization and extra keywords; the sharing optimization applies to
209
+ the exact base class.
166
210
 
167
211
  Renderers may return a String or nil through their existing methods. New
168
212
  renderers return either `{status: :code, code: source}` or
@@ -173,13 +217,21 @@ overview and every panel before rendering.
173
217
 
174
218
  Generated-client adapters belong to their generator gems and consume context
175
219
  readers without depending on the server gem. They resolve names using the
176
- complete private generator model and never run a compiler, formatter or generator
177
- in an HTML request. Keep adapter rendering read-only: configured adapters are frozen
178
- and shared, while filtered contexts are copied per request.
220
+ complete private generator model and never run a compiler, formatter or
221
+ generator
222
+ in an HTML request. Keep adapter rendering read-only: configured adapters are
223
+ frozen
224
+ and shared, while authorized description snapshots belong to individual
225
+ requests.
226
+ Derived contexts within one request share unchanged owned data. Supplying a
227
+ resource, action or sample still copies that supplied subtree, so a resource
228
+ with
229
+ many actions can remain costly to render.
179
230
 
180
231
  ## Verification
181
232
 
182
- `make test-documentation` checks renderers, contexts, profiles, rendered HTML and
233
+ `make test-documentation` checks renderers, contexts, profiles, rendered HTML
234
+ and
183
235
  local assets. Prepare the component bundles and JavaScript development
184
236
  dependencies with `make release-deps` first. The target builds the JavaScript
185
237
  package before compiling TypeScript examples against its public declarations;
@@ -190,6 +242,107 @@ against the real request encoder. It requires the locked Python environment
190
242
  prepared by `make release-deps` and is included in `make test`. The ordinary
191
243
  Ruby renderer checks run without the Python package environment.
192
244
 
245
+ Deterministic context and HTML regressions run in the ordinary server suite.
246
+ They check description sharing, allocation growth and isolation between users
247
+ without a wall-clock threshold. The Rust adapter suite separately checks that
248
+ normalization avoids unrelated description descendants.
249
+
250
+ ### Documentation benchmark
251
+
252
+ Prepare the server bundle in its declared Nix environment, then run the opt-in
253
+ harness from the repository root:
254
+
255
+ ```sh
256
+ nix develop .#server-ruby --command bash -c \
257
+ 'make --no-print-directory benchmark-documentation > /tmp/documentation.json'
258
+ ```
259
+
260
+ The target synchronizes shared documentation before running the harness.
261
+ Standard
262
+ output is JSON; shell/dependency preparation happens before the redirected
263
+ command.
264
+ No network server, database, generated SDK compilation or formatter is involved.
265
+ The synthetic fixture uses Basic users `admin` and `member`, both with the local
266
+ test password `fixture-only`.
267
+
268
+ Defaults measure two shapes with eight ordinary actions each: eight resources
269
+ with one action, and one resource with eight actions. Each ordinary action also
270
+ has a privileged sample; the first resource has an additional admin-only action.
271
+ Context lanes derive a prepared parent with a changed profile or a constant
272
+ small
273
+ sample. The HTML lane performs one real Rack request per trial, with all
274
+ available
275
+ dynamic renderer panels. Use a minimal smoke run or a larger repeated campaign:
276
+
277
+ ```sh
278
+ make benchmark-documentation \
279
+ BENCHMARK_DOCUMENTATION_ARGS='--shape resources --size 1 --warmup 0 --trials 1 --iterations 1'
280
+ make benchmark-documentation \
281
+ BENCHMARK_DOCUMENTATION_ARGS='--shape both --size 64 --samples 2 --parameters 8 --warmup 2 --trials 7 --iterations 500' \
282
+ > /tmp/documentation-large.json
283
+ ```
284
+
285
+ Run these `make` commands inside the prepared shell. `--shape custom --resources
286
+ N --actions N` controls both dimensions independently; `--lanes context`
287
+ isolates
288
+ derivation and `--lanes html` isolates requests. `--user member` selects
289
+ restricted
290
+ help. `--help` lists all options. `--rust` enables the fixture's matching Rust
291
+ profile, includes both Rust variants in HTML, and adds separate async/blocking
292
+ action and authentication adapter lanes. It requires the optional generator's
293
+ Ruby dependencies to be available; ordinary benchmark runs need none of them.
294
+ Prepare both component environments using `nix develop .#server-ruby` and
295
+ `nix develop .#client-rust`. The server bundle alone does not activate the Rust
296
+ dependencies. In the top-level Nix shell, combine the prepared local gem stores:
297
+
298
+ ```sh
299
+ env -u RUBYOPT -u RUBYLIB -u BUNDLE_BIN_PATH -u BUNDLE_GEMFILE \
300
+ -u BUNDLE_LOCKFILE -u BUNDLE_PATH -u BUNDLE_FROZEN -u BUNDLER_SETUP \
301
+ GEM_PATH="$PWD/.gems:$PWD/clients/rust/.gems:$(ruby -e 'puts Gem.default_dir')" \
302
+ make benchmark-documentation BENCHMARK_DOCUMENTATION_RUBY=ruby \
303
+ BENCHMARK_DOCUMENTATION_ARGS='--rust --shape both --size 8 --warmup 1 --trials 3 --iterations 100' \
304
+ > /tmp/documentation-rust.json
305
+ ```
306
+
307
+ This command runs outside Bundler and records the gem versions actually
308
+ activated. Retain that list alongside both component locks and use the same
309
+ prepared inputs for both revisions. Missing optional dependencies fail the run;
310
+ Rust lanes are never silently omitted. See the
311
+ [Rust adapter guide](../clients/rust/docs/documentation.md#verification).
312
+
313
+ Every lane reports each trial and the median wall seconds, process CPU seconds
314
+ and allocated objects, with the operation count per trial. Wall time uses a
315
+ monotonic clock. A full GC runs before each trial, outside its timer; GC
316
+ triggered
317
+ by the measured work remains included. Mount/profile construction has separate
318
+ setup metrics. Ownership construction, description selection, warmups, output
319
+ validation, hashing and JSON serialization are outside lane measurements.
320
+ Reports include fixture dimensions, renderer IDs, profile mode, Ruby runtime,
321
+ host, Git HEAD/status/diff digest, loaded source digests, dependency versions
322
+ and
323
+ available lockfile/harness/fixture digests. HTML and Rust outputs include
324
+ status, bytes and
325
+ SHA-256; failed or missing output causes a nonzero exit.
326
+
327
+ For a before/after comparison, retain one exact harness and fixture and point
328
+ them at each implementation with `--source-root /absolute/checkout`. Set up
329
+ equivalent dependencies and runtime first. An explicit `--source-revision LABEL`
330
+ can supplement the detected HEAD and is required for an exported source tree.
331
+ Retain exact heads, commands, raw JSON, lock/input identities and host identity.
332
+ Compare dimensions, renderer modes, statuses, bytes and body digests before
333
+ comparing medians. Context `description_shared` deliberately distinguishes the
334
+ old copying implementation. Investigate any output difference; record exact
335
+ volatile-field normalization if needed rather than silently dropping mismatches.
336
+
337
+ These measurements describe the synthetic workload on the recorded host. They
338
+ do not establish production latency. The opt-in harness has no timing threshold;
339
+ the [paired endpoint performance suite](performance.md) supplies required
340
+ quick/weekly CI timing, allocation and query gates, including documentation
341
+ OPTIONS and both HTML shapes. It retains baseline/self noise and blocks
342
+ inconclusive results. Ownership, traversal and allocation regressions cover this
343
+ failure class, while concentrated-resource costs and other future regressions
344
+ still need representative measurements.
345
+
193
346
  Each client also has acceptance commands that check executable snippets against
194
347
  installed packages or a generated SDK. These use
195
348
  rendered source against disposable fixtures; syntax checks alone do not
@@ -0,0 +1,310 @@
1
+ # Endpoint performance checks
2
+
3
+ The Ruby performance suite sends real Rack requests through routing,
4
+ authentication, authorization, typed validation and serialization. It covers
5
+ public, Basic and token requests, valid and invalid typed input, nested resources,
6
+ lists, SQLite pagination and eager loading, and documentation OPTIONS/HTML.
7
+ Deterministic specs check response contracts, isolation, allocation growth and
8
+ query/authorization counts. Paired timing runs compare exact baseline and
9
+ candidate sources with one neutral harness and third-party dependency graph.
10
+
11
+ These are synthetic in-process workloads. SQLite results describe the fixture's
12
+ query behavior; they do not measure production database, network server or
13
+ vpsAdmin latency. The checks detect specific regressions and do not guarantee
14
+ every future performance characteristic.
15
+
16
+ ## Local commands
17
+
18
+ The benchmark has its own third-party tool bundle and pinned Nix shell in
19
+ `servers/ruby/benchmarks/`. This keeps contributor Nix updates separate from the
20
+ calibrated measurement runtime. Prepare the ordinary frozen bundle once:
21
+
22
+ ```sh
23
+ export HAVEAPI_BENCHMARK_GEM_HOME="$PWD/servers/ruby/benchmarks/.gems"
24
+ nix-shell servers/ruby/benchmarks/shell.nix --run '
25
+ set -e
26
+ gem install bundler --version 4.0.23 --no-document
27
+ BUNDLE_IGNORE_CONFIG=1 BUNDLE_FROZEN=1 BUNDLE_PATH__SYSTEM=1 \
28
+ BUNDLE_GEMFILE="$PWD/servers/ruby/benchmarks/Gemfile" bundle _4.0.23_ install
29
+ '
30
+ ```
31
+
32
+ The tool Gemfile installs no HaveAPI package. Its tracked lock is an application
33
+ lock; component library locks remain local contributor inputs. Launch the runner
34
+ outside Bundler so both exported sources use the same Ruby and gem paths. The
35
+ shell clears inherited Bundler settings, `RUBYOPT`, `RUBYLIB` and
36
+ `RUBYGEMS_GEMDEPS`.
37
+
38
+ Run deterministic specs in the prepared contributor shell with
39
+ `make test-performance`. For a comparison, keep the neutral harness in a candidate
40
+ checkout containing both exact Git objects. Export the baseline and candidate
41
+ separately, retain their archive hashes and manifests, and run `make doc` in each
42
+ exported root. Then run from the candidate checkout:
43
+
44
+ ```sh
45
+ nix-shell servers/ruby/benchmarks/shell.nix --run '
46
+ make performance PERFORMANCE_ARGS="--baseline-root /absolute/baseline --baseline-sha BASELINE_FULL_SHA --candidate-root /absolute/candidate --candidate-sha CANDIDATE_FULL_SHA --profile quick --budgets servers/ruby/benchmarks/performance_budgets.json --output /absolute/new-evidence-directory"
47
+ '
48
+ ```
49
+
50
+ Replace paths and full 40-character lowercase SHAs. Use `--profile weekly` for
51
+ the expanded workload. The runner checks the exact calibrated Ruby and available
52
+ dependency versions before timing, then requires each worker's complete activated
53
+ graph to match the weekly calibration. Source and loaded-file checks and
54
+ response validation must also pass. An unsupported declaration or runtime change
55
+ requires a separately reviewed environment migration; it does not update the
56
+ reference or budgets.
57
+
58
+ To reclassify retained samples without making requests:
59
+
60
+ ```sh
61
+ make performance-compare \
62
+ PERFORMANCE_ARGS='--input /absolute/evidence/samples.json --budgets servers/ruby/benchmarks/performance_budgets.json --output /absolute/new-comparison-directory'
63
+ ```
64
+
65
+ The commands invoke `servers/ruby/benchmarks/endpoints.rb run` and `compare`
66
+ directly. `run` retains `samples.json`, `report.json` and process diagnostics.
67
+ `compare` reads compact samples format version 2; unsupported versions are
68
+ rejected without conversion. `report.json` retains the original measurement's
69
+ `harness_sha256`. Its `comparison` metadata identifies policy
70
+ `per-revision-response-stability-v1`, the loaded comparator file's SHA-256 and
71
+ the SHA-256 of the exact consumed `samples.json` bytes. Error reports retain
72
+ these identities when available; they omit unavailable measurement/input fields
73
+ instead of identifying old measurements with the current harness. The existing
74
+ `budget_sha256` identifies the budget. Replaying samples classifies the recorded
75
+ measurements under the identified comparison policy. It makes no requests and
76
+ leaves the original result unchanged.
77
+ `report.json` contains source/profile identity, overall outcome and per-workload
78
+ results, without raw samples or response bodies.
79
+ Exit codes are 0 for a pass
80
+ or an inconclusive timing result, 1 for a regression and 3 for errors, including
81
+ missing workloads, invalid or unstable responses, inputs or provenance. An
82
+ inconclusive report remains explicitly inconclusive; a successful process exit
83
+ does not establish a measured performance pass. Deterministic failures and regressions block CI.
84
+ Keep the original evidence and diagnose failures or insufficient timing signal;
85
+ there is no automatic retry loop or baseline update.
86
+
87
+ ## Timing and work gates
88
+
89
+ Quick workloads include seven API cases, SQLite page/includes cases, and
90
+ documentation OPTIONS plus both HTML shapes at size 8. List and includes quick
91
+ cases use 32 rows; pagination uses page size 32 over 128 rows. Weekly adds
92
+ member/admin variants, larger payloads, list/includes output up to 1,024 rows,
93
+ page size 32 over up to 10,000 SQLite rows, documentation shapes at size 16 and
94
+ Rust async/blocking action/authentication examples. An output of 1,024 rows uses
95
+ an unpaginated relation; it is not an invalid request above the pagination limit.
96
+
97
+ Both profiles also render complete Rust-enabled HTML pages for admin and member
98
+ users. Quick uses 16 resources; weekly uses 32. Each resource has four actions,
99
+ two samples per action and four parameters. These cases validate the full page,
100
+ Rust panels, authorization boundaries and operation counts. They use relative
101
+ allocation and deterministic checks without inventing fixed-reference allocation
102
+ ceilings for the new shapes. Existing calibrated workload budgets remain intact.
103
+
104
+ These shapes exercise complete pages with several megabytes of HTML, so a
105
+ single request provides substantial measurement work. The weekly shape doubles
106
+ the resource count. Actual batch durations and output sizes belong in each
107
+ run's evidence; these synthetic shapes do not define production latency targets.
108
+
109
+ | Measurement policy | Quick | Weekly |
110
+ | --- | --- | --- |
111
+ | Baseline request work per batch | 1 second | 2 seconds |
112
+ | Minimum adequate observed batch | 750 ms | 1.5 seconds |
113
+ | Baseline/self pairs | 3 | 3 |
114
+ | Baseline/candidate pairs | 5 | 7 |
115
+ | Required agreement | 4 of 5 | 6 of 7 |
116
+ | Relative wall/CPU regression budget | 20% | 10% |
117
+ | Minimum detectable batch increase | 50 ms | 50 ms |
118
+
119
+ A baseline pilot selects a common count of 1–100,000 requests for both revisions.
120
+ One bounded calibration adjustment can increase an undersized baseline batch.
121
+ Reports retain observed duration, chosen count, adjustment and any cap limitation;
122
+ a capped batch below the adequacy threshold is insufficient timing evidence.
123
+ Cheap workloads have two warmup batches of about 100 ms each, outside the
124
+ measurement. If the baseline pilot already meets the profile's work target with
125
+ one expensive request, both revisions use count 1 and no extra request warmups.
126
+ That case measures the first complete request after equivalent fixture setup;
127
+ the candidate cannot choose a different warmup policy.
128
+
129
+ Wall and process CPU time are compared separately after normalization per request.
130
+ The minimum detectable increase is the larger of the profile's relative budget
131
+ and 50 ms divided by the common request count. Paired baseline/self observations
132
+ retain the measured noise. A consistent increase beyond the budget and noise
133
+ fails. Reports distinguish ordinary variation from insufficient timing signal.
134
+ Inconclusive timing remains labeled as such; it does not trigger automatic
135
+ workflow retries. The agreement rules are engineering thresholds, not statistical
136
+ confidence levels.
137
+
138
+ Candidate order alternates. Setup, mount, schema, seeds, token issuance, reset,
139
+ response parsing/digests and counter checks stay outside the timed loop. GC
140
+ remains enabled, including collections triggered by measured requests. A
141
+ 30-second watchdog surrounds each request batch, including pilot and warmups.
142
+ This also bounds every individual request without instrumenting the cheap request
143
+ loop. It is supplementary runaway protection, not a regression budget. Setup has
144
+ a separate 120-second timeout. Supervision records source, workload and phase and terminates
145
+ and reaps stuck workers.
146
+
147
+ Within each measurement or warmup, the worker validates each distinct raw body
148
+ once for that scenario and retains an immutable proof of its parsed values and
149
+ output semantics. Every operation still checks the raw digest and exact bytes,
150
+ HTTP status and content type, called index, and execution/authorization/query
151
+ records. A hash collision cannot reuse a proof. Proofs are discarded at the
152
+ sample boundary and never shared between users, scenarios or source revisions.
153
+ Samples retain metrics, one uniform validated counter record with its operation
154
+ count, and distinct response examples with occurrence counts and raw bodies.
155
+ Replay checks those counts, counters, examples and raw/canonical identities,
156
+ including comparison mode, verified own version and source/environment/inputs.
157
+ It compares canonical response identities and occurrence totals across samples
158
+ from the same verified source SHA. Both columns of a baseline/self pair belong
159
+ to the baseline SHA. JSON map order may vary; raw byte stability is not required.
160
+ It does not retain request order or independently reprove each live
161
+ authorization/counter check.
162
+ The compact artifact is a summary, not an execution transcript.
163
+
164
+ Responses may differ between baseline and candidate without failing or skipping
165
+ the performance comparison. Each workload reports `raw_changed` and
166
+ `canonical_changed`, together with baseline/candidate bytes per request and
167
+ their byte and percentage changes. These diagnostics use the
168
+ occurrence-weighted output distributions from baseline/candidate pairs only;
169
+ baseline/self pairs do not affect the size comparison. Reports retain no raw
170
+ bodies. Response growth alone does not fail the comparison or change its timing
171
+ and allocation limits. Equal response lengths do not prove correctness. The CI
172
+ step summary shows these diagnostics beside wall, CPU, allocation and overall
173
+ outcomes, including regressions and inconclusive timing.
174
+
175
+ Allocation growth above baseline median plus the larger of 10% or 32 objects
176
+ per request fails. Additional fixed-reference ceilings use measured accepted
177
+ counts with 25% headroom plus 32 objects/request. Exact SQL query and required
178
+ authorization counts prevent a fast but incomplete operation from passing.
179
+ Scaling bounds reflect actual output work; growing responses need not allocate
180
+ a constant amount. Budget changes require measured justification and review.
181
+ Missing or uncalibrated budgets for the existing calibrated workloads fail rather
182
+ than disabling the gate.
183
+
184
+ Both profiles use the recorded weekly Rust dependency closure as their expected
185
+ runtime identity. Quick includes the Rust manifests, gemspec and loaded
186
+ dependencies. Both profiles retain the benchmark Gemfile, lock and shell bytes
187
+ and hashes. Baseline and candidate must match that environment exactly;
188
+ unavailable or changed identities fail. The shared
189
+ comparison entry point activates the recorded native JSON 2.21.2 before its
190
+ first require, including in unbundled workers. A newer visible or already loaded
191
+ JSON must not silently replace that dependency; a missing or conflicting version
192
+ fails during activation. Historical
193
+ quick allocation calibration and all existing reference/budget bytes are retained
194
+ unchanged, without claiming that the original quick calibration loaded Rust.
195
+
196
+ The root, Ruby server and Rust generator development Gemfiles declare JSON
197
+ 2.21.2 so fresh setup installs the recorded native dependency. Comparisons
198
+ accept only that exact declaration change in these three files. They retain
199
+ the original manifest contents, hashes and declaration evidence, then compare
200
+ normalized contents. Replay verifies the transformation from the retained raw
201
+ inputs. Other dependency requirements and manifest contents must match.
202
+
203
+ ## GitHub workflow and baseline selection
204
+
205
+ The `servers/ruby performance` workflow runs on pushes, ordinary pull requests,
206
+ manual dispatch and Sunday at 03:00 UTC. It always creates the `Performance gate`
207
+ aggregate check. Relevant-path detection happens after event resolution, inside
208
+ the workflow; an irrelevant valid change can succeed without running benchmarks.
209
+ Source/dependency/fixture/budget/workflow/flake changes trigger measurement.
210
+ Missing event/source evidence fails even when no relevant path can be established.
211
+
212
+ | Event | Candidate | Baseline |
213
+ | --- | --- | --- |
214
+ | Pull request | Exact event head SHA | Merge-base of head and exact event base SHA |
215
+ | Existing branch push, quick | Exact event `after` | Exact nonzero event `before`, on every branch |
216
+ | New branch push, quick (zero `before`) | Exact event `after` | Merge-base with captured exact default-branch SHA |
217
+ | Weekly | Exact scheduled default-branch event SHA | Fixed accepted reference below |
218
+ | Manual with a complete explicit pair | Supplied full candidate SHA | Supplied full baseline SHA |
219
+ | Manual without either SHA | Captured default SHA | Fixed accepted reference below |
220
+
221
+ The immutable accepted reference is
222
+ `b85ec364057f536940d689b09e612512ffffaf82`. Required ancestry is checked. The
223
+ reference and budgets never move automatically. Deleted branches, malformed or
224
+ missing commits, unavailable new-branch fallback, partial manual pairs and equal
225
+ endpoints fail. PRs use the head commit, never the synthetic merge commit. An
226
+ existing branch push uses `before`, never `HEAD^` or a guessed fallback. A quick
227
+ push can compare a nonancestor before/after pair after a force push; its exact
228
+ event pair is carried through all ancestry guards. Direct CLI use of that
229
+ exception requires `--push-event selection.json` from the event resolver. Other
230
+ PR, manual and weekly ancestry restrictions remain. Relevance uses the direct
231
+ before/after tree diff, including the benchmark setup files.
232
+ The weekly profile always requires that fixed baseline, including an explicit
233
+ manual pair. An explicit quick pair may select another ancestor.
234
+
235
+ ### Maintained 0.30 release comparisons
236
+
237
+ The original fixed reference remains the measured baseline for weekly runs.
238
+ Its maintenance counterpart is
239
+ `8b7abc097db49893da9cc8fa4395648901fec71d`. The resolver and direct runner accept
240
+ this one nonliteral ancestry relationship only when the counterpart is a literal
241
+ candidate ancestor and the complete binary tree delta from the original reference
242
+ equals the published 0.30.1 metadata commit
243
+ `1c3c98f0d77e0b23c8da1a8937f25c5799864a3b` relative to its parent. They verify
244
+ the exact four trees and canonical full-index delta hash, retain both raw diffs
245
+ and bind the receipt to
246
+ the candidate SHA. Cherry-pick trailers do not establish this proof. A different
247
+ mapping needs a new review; the reference and budget calibration never change.
248
+ Diff rendering uses full blob IDs and explicit formatting options, independent
249
+ of repository-dependent abbreviation and user diff display settings. Ordinary
250
+ master ancestry is checked before probing the maintenance mapping, so a master
251
+ clone does not need maintenance objects for that path.
252
+ Unrelated direct quick baselines still fail. Existing maintenance branch pushes
253
+ use their exact pushed pair; new branches retain the captured-default fallback
254
+ and reason. A `haveapi-0.30` push still requires verified maintenance lineage.
255
+
256
+ Both sources in a maintenance comparison use the prepared benchmark environment.
257
+ In addition to the JSON preparation rule above, these comparisons normalize the
258
+ shared `0.30.x` patch identity, matching local Ruby component VERSION assignments
259
+ and exactly synchronized internal `haveapi-client` requirements. All remaining
260
+ bytes in dependency manifests, gemspecs and Ruby requirements must match, as must
261
+ the resolved third-party runtime graph and benchmark lock bytes. Source `flake.nix` and `flake.lock` remain raw provenance
262
+ with source hashes; they do not supply the benchmark runtime and may differ.
263
+ Every other supported declaration remains strict. The consumed benchmark shell
264
+ and lock are retained separately and must match across workers. Raw manifest
265
+ contents and hashes remain bound to each verified source tree alongside their
266
+ normalized hashes. Replay
267
+ recomputes the transformation and rejects missing, inconsistent or changed inputs.
268
+
269
+ The HTML comparison may normalize the own-version segment of the documentation
270
+ templates' `https://webui.haveapi.org/v<own-version>/#...` anchor hrefs and
271
+ the version in the exact framework-generated HaveAPI footer. The footer must
272
+ occur once and match the verified source's own VERSION; literal examples, other
273
+ footer text and URLs remain exact. Validation runs after the timed loop. Complete
274
+ raw bodies, byte counts and hashes remain in the samples, together with the
275
+ normalized comparison digest and source version. Replay checks these again from
276
+ the raw body. This normalization applies only to the HTML documentation workloads
277
+ on the proven maintenance lineage. JSON/API
278
+ values, other URLs and hosts, example code, authorization markers, panels and all
279
+ other output are not normalized. Cross-revision differences remain diagnostic;
280
+ within-revision stability and raw/canonical integrity still apply.
281
+
282
+ The workflow uses ephemeral GitHub-hosted Ubuntu workers, `contents: read`,
283
+ disabled checkout credential persistence, ordinary `pull_request` events and no
284
+ secrets, publication context or PR comments. The comparison job has a hard
285
+ 15-minute quick limit and 45-minute weekly limit. Measured page sizes and sample
286
+ counts must leave room for preparation, ordinary workloads and evidence
287
+ validation within those limits. Actual hosted campaign duration requires its own
288
+ run receipt; the page probe alone does not prove it. A timeout blocks and needs
289
+ diagnosis. Superseded runs are cancelled only within
290
+ the same event/branch or PR concurrency group.
291
+
292
+ Success, regression, inconclusive and preparation-failure artifacts are retained
293
+ for 30 days. They include event selection, exact SHAs and source trees/archive
294
+ hashes, matched dependency manifests, resolved locks, runtime/input graphs, raw
295
+ samples, reports, process diagnostics and runner exit. The aggregate rejects
296
+ failed, cancelled or unexpectedly skipped required work and requires the retained
297
+ report. A completed comparison can retain inconclusive timing diagnostics while
298
+ its deterministic checks pass; the report keeps that distinction visible.
299
+ Repository rules determine whether this aggregate check is required for merging;
300
+ the workflow does not change branch protection or repository permissions.
301
+
302
+ A newly added workflow's manual dispatch may be unavailable until its definition
303
+ exists on the default branch. Preserve a dispatch refusal and run the same weekly
304
+ CLI locally if needed; disclose that hosted weekly execution remains unverified.
305
+ Workflow enablement alone is not a reason to integrate source into the default
306
+ branch. Push verification still checks the exact published candidate.
307
+
308
+ The separate [documentation benchmark](client-examples.md#documentation-benchmark)
309
+ remains available for diagnosis and custom page shapes. Its JSON trials/medians
310
+ are useful measurements; the paired endpoint runner owns required timing gates.
data/haveapi.gemspec CHANGED
@@ -15,7 +15,7 @@ Gem::Specification.new do |s|
15
15
  s.required_ruby_version = ">= #{File.read('../../.ruby-version').strip}"
16
16
 
17
17
  s.add_dependency 'activesupport', '>= 7.1'
18
- s.add_dependency 'haveapi-client', '~> 0.30.1'
18
+ s.add_dependency 'haveapi-client', '~> 0.31.0'
19
19
  s.add_dependency 'i18n', '>= 1.6', '< 2'
20
20
  s.add_dependency 'json'
21
21
  s.add_dependency 'mail'