hecks 2.7.0 → 2.9.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 (463) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +11 -0
  3. data/lib/hecks/adapters/driven/claude_code.rb +4 -104
  4. data/lib/hecks/adapters/driven/d1.rb +44 -237
  5. data/lib/hecks/adapters/driven/folder.rb +17 -76
  6. data/lib/hecks/adapters/driven/google_authentication.rb +15 -49
  7. data/lib/hecks/adapters/driven/governance_authorization.rb +23 -70
  8. data/lib/hecks/adapters/driven/heki/journal.rb +8 -41
  9. data/lib/hecks/adapters/driven/heki/saga_store.rb +12 -54
  10. data/lib/hecks/adapters/driven/heki/snapshot.rb +4 -12
  11. data/lib/hecks/adapters/driven/heki.rb +13 -42
  12. data/lib/hecks/adapters/driven/identity_registry.rb +3 -14
  13. data/lib/hecks/adapters/driven/in_memory_ordering.rb +9 -26
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +2 -14
  15. data/lib/hecks/adapters/driven/lambda/client.rb +10 -51
  16. data/lib/hecks/adapters/driven/lambda.rb +10 -84
  17. data/lib/hecks/adapters/driven/local_storage.rb +5 -40
  18. data/lib/hecks/adapters/driven/memory.rb +18 -99
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +5 -21
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +6 -28
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +3 -12
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +4 -30
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +34 -108
  24. data/lib/hecks/adapters/driven/postgres.rb +145 -134
  25. data/lib/hecks/adapters/driven/postgres_era.adapter +4 -15
  26. data/lib/hecks/adapters/driven/prism.rb +7 -30
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +21 -121
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +8 -40
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +26 -82
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +40 -84
  31. data/lib/hecks/adapters/driven/sqlite.rb +128 -65
  32. data/lib/hecks/adapters/driven/tenant_provisioner.rb +15 -37
  33. data/lib/hecks/adapters/driven.rb +5 -36
  34. data/lib/hecks/adapters/driving/github_webhook.rb +8 -76
  35. data/lib/hecks/behaviors/dsl.rb +7 -25
  36. data/lib/hecks/behaviors/expectations.rb +32 -142
  37. data/lib/hecks/behaviors/ir.rb +7 -20
  38. data/lib/hecks/behaviors/rspec.rb +6 -17
  39. data/lib/hecks/behaviors/runner.rb +10 -34
  40. data/lib/hecks/behaviors.rb +4 -8
  41. data/lib/hecks/bluebook/aggregate.rb +9 -75
  42. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +9 -38
  43. data/lib/hecks/bluebook/assembly/build.rb +4 -15
  44. data/lib/hecks/bluebook/assembly/contract.rb +15 -136
  45. data/lib/hecks/bluebook/assembly/contracts.rb +54 -174
  46. data/lib/hecks/bluebook/assembly/marks.rb +31 -223
  47. data/lib/hecks/bluebook/assembly/specializer.rb +6 -52
  48. data/lib/hecks/bluebook/assembly.rb +6 -45
  49. data/lib/hecks/bluebook/attribute.rb +12 -61
  50. data/lib/hecks/bluebook/behaviour/aggregate.rb +10 -39
  51. data/lib/hecks/bluebook/behaviour/attribute.rb +3 -14
  52. data/lib/hecks/bluebook/behaviour/chapter.rb +17 -42
  53. data/lib/hecks/bluebook/behaviour/command.rb +14 -66
  54. data/lib/hecks/bluebook/behaviour/domain_port.rb +4 -20
  55. data/lib/hecks/bluebook/behaviour/entity.rb +7 -27
  56. data/lib/hecks/bluebook/behaviour/hexagon.rb +23 -41
  57. data/lib/hecks/bluebook/behaviour/lifecycle.rb +15 -40
  58. data/lib/hecks/bluebook/behaviour/policy.rb +23 -52
  59. data/lib/hecks/bluebook/behaviour/process_manager.rb +30 -57
  60. data/lib/hecks/bluebook/behaviour/read_model.rb +38 -26
  61. data/lib/hecks/bluebook/behaviour/traits.rb +27 -64
  62. data/lib/hecks/bluebook/behaviour/value_object.rb +5 -24
  63. data/lib/hecks/bluebook/capabilities.rb +17 -49
  64. data/lib/hecks/bluebook/chapter.rb +10 -48
  65. data/lib/hecks/bluebook/command.rb +23 -81
  66. data/lib/hecks/bluebook/domain_port.rb +14 -63
  67. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +50 -229
  68. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +50 -377
  69. data/lib/hecks/bluebook/dsl/attribute_collector.rb +33 -204
  70. data/lib/hecks/bluebook/dsl/binding_proxy.rb +17 -65
  71. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +86 -708
  72. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +56 -170
  73. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +1 -0
  74. data/lib/hecks/bluebook/dsl/command_builder.rb +74 -546
  75. data/lib/hecks/bluebook/dsl/const_shim.rb +8 -41
  76. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +12 -87
  77. data/lib/hecks/bluebook/dsl/entity_builder.rb +44 -330
  78. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +20 -269
  79. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +26 -189
  80. data/lib/hecks/bluebook/dsl/identity_declaration.rb +13 -88
  81. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +6 -20
  82. data/lib/hecks/bluebook/dsl/policy_builder.rb +16 -88
  83. data/lib/hecks/bluebook/dsl/port_builder.rb +2 -6
  84. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +22 -96
  85. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +31 -184
  86. data/lib/hecks/bluebook/dsl/query_builder.rb +11 -42
  87. data/lib/hecks/bluebook/dsl/read_model_builder.rb +75 -201
  88. data/lib/hecks/bluebook/dsl/rule_reference.rb +22 -163
  89. data/lib/hecks/bluebook/dsl/translation_builder.rb +28 -114
  90. data/lib/hecks/bluebook/dsl/value_object_builder.rb +10 -124
  91. data/lib/hecks/bluebook/dsl/word_gate.rb +16 -162
  92. data/lib/hecks/bluebook/dsl/world_builder.rb +19 -106
  93. data/lib/hecks/bluebook/entity.rb +13 -71
  94. data/lib/hecks/bluebook/expression/ast_json.rb +24 -190
  95. data/lib/hecks/bluebook/expression/ast_reader.rb +12 -54
  96. data/lib/hecks/bluebook/expression/canonical_form.rb +11 -88
  97. data/lib/hecks/bluebook/expression/evaluator.rb +47 -295
  98. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +30 -147
  99. data/lib/hecks/bluebook/expression/resolver.rb +37 -660
  100. data/lib/hecks/bluebook/hexagon.rb +5 -28
  101. data/lib/hecks/bluebook/lifecycle.rb +5 -16
  102. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -7
  103. data/lib/hecks/bluebook/meta_validator/judge.rb +111 -361
  104. data/lib/hecks/bluebook/meta_validator/plan.rb +33 -162
  105. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -7
  106. data/lib/hecks/bluebook/meta_validator/readings.rb +56 -342
  107. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -176
  108. data/lib/hecks/bluebook/meta_validator/shapes.rb +54 -268
  109. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +47 -308
  110. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +7 -43
  111. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -18
  112. data/lib/hecks/bluebook/meta_validator.rb +79 -349
  113. data/lib/hecks/bluebook/model_check/client_profile.rb +10 -91
  114. data/lib/hecks/bluebook/model_check.rb +102 -600
  115. data/lib/hecks/bluebook/pattern_subset.rb +11 -69
  116. data/lib/hecks/bluebook/process_manager.rb +13 -83
  117. data/lib/hecks/bluebook/project_loader.rb +3 -39
  118. data/lib/hecks/bluebook/project_register.rb +12 -60
  119. data/lib/hecks/bluebook/query.rb +9 -48
  120. data/lib/hecks/bluebook/read_model.rb +57 -81
  121. data/lib/hecks/bluebook/reference.rb +7 -41
  122. data/lib/hecks/bluebook/smoke_test.rb +8 -98
  123. data/lib/hecks/bluebook/synthesizer.rb +13 -62
  124. data/lib/hecks/bluebook/translation.rb +26 -62
  125. data/lib/hecks/bluebook/value_object.rb +11 -36
  126. data/lib/hecks/bluebook.rb +5 -22
  127. data/lib/hecks/cache_dir.rb +84 -0
  128. data/lib/hecks/cli/document.rb +86 -0
  129. data/lib/hecks/cli/ir.rb +45 -0
  130. data/lib/hecks/cli/mcp.rb +31 -0
  131. data/lib/hecks/cli/mcp_door.rb +366 -0
  132. data/lib/hecks/cli/model_check.rb +203 -0
  133. data/lib/hecks/cli/project_cli.rb +90 -0
  134. data/lib/hecks/cli/project_diagrams.rb +36 -0
  135. data/lib/hecks/cli/run.rb +173 -0
  136. data/lib/hecks/cli/smoke_test.rb +45 -0
  137. data/lib/hecks/cli/stores.rb +53 -0
  138. data/lib/hecks/cli.rb +160 -0
  139. data/lib/hecks/construct.rb +8 -34
  140. data/lib/hecks/deploy/bluebook/deploy.bluebook +33 -142
  141. data/lib/hecks/deploy/bluebook/deploy.hecksagon +2 -9
  142. data/lib/hecks/embryonaut_bluebook/lock.rb +3 -35
  143. data/lib/hecks/embryonaut_bluebook/shape.rb +1 -17
  144. data/lib/hecks/embryonaut_bluebook/vendor.rb +8 -53
  145. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +4 -19
  146. data/lib/hecks/embryonaut_bluebook.rb +4 -71
  147. data/lib/hecks/facade/cli_door.rb +24 -106
  148. data/lib/hecks/facade/cli_runner.rb +28 -172
  149. data/lib/hecks/facade/command_request.rb +13 -33
  150. data/lib/hecks/facade/handle.rb +34 -121
  151. data/lib/hecks/facade/json_door.rb +31 -146
  152. data/lib/hecks/facade/surface/aggregate_door.rb +39 -104
  153. data/lib/hecks/facade/surface/chapter.rb +10 -54
  154. data/lib/hecks/facade/surface.rb +8 -23
  155. data/lib/hecks/facade.rb +10 -25
  156. data/lib/hecks/forms/app.rb +15 -70
  157. data/lib/hecks/forms/command_form_renderer.rb +10 -29
  158. data/lib/hecks/forms/field_renderer.rb +17 -156
  159. data/lib/hecks/forms/field_shape.rb +30 -222
  160. data/lib/hecks/forms/html.rb +12 -47
  161. data/lib/hecks/forms/index_renderer.rb +1 -4
  162. data/lib/hecks/forms/page.rb +3 -9
  163. data/lib/hecks/forms/params.rb +14 -60
  164. data/lib/hecks/forms/port_argument.rb +2 -19
  165. data/lib/hecks/forms/query_form_renderer.rb +4 -79
  166. data/lib/hecks/forms/record_renderer.rb +8 -75
  167. data/lib/hecks/forms/record_table.rb +7 -50
  168. data/lib/hecks/forms/reference_options.rb +3 -30
  169. data/lib/hecks/forms/value_object_shape.rb +8 -35
  170. data/lib/hecks/forms.rb +5 -33
  171. data/lib/hecks/fqn.rb +5 -12
  172. data/lib/hecks/framework/bluebook/compliance.bluebook +16 -65
  173. data/lib/hecks/framework/bluebook/console_settings.bluebook +47 -134
  174. data/lib/hecks/framework/bluebook/framework.hecksagon +5 -14
  175. data/lib/hecks/framework/bluebook/governance.bluebook +13 -34
  176. data/lib/hecks/framework/bluebook/identity.bluebook +11 -26
  177. data/lib/hecks/framework/bluebook/privacy.bluebook +8 -33
  178. data/lib/hecks/framework.rb +11 -80
  179. data/lib/hecks/freezer.rb +5 -39
  180. data/lib/hecks/grammar/expression.bluebook +15 -40
  181. data/lib/hecks/grammar/translation.bluebook +3 -6
  182. data/lib/hecks/grammar.rb +16 -41
  183. data/lib/hecks/ir.rb +14 -83
  184. data/lib/hecks/language/adapter.bluebook +2 -7
  185. data/lib/hecks/language/bluebook/aggregate.bluebook +56 -221
  186. data/lib/hecks/language/bluebook/attaches/paging.bluebook +6 -15
  187. data/lib/hecks/language/bluebook/bluebook.bluebook +15 -83
  188. data/lib/hecks/language/bluebook/bluebook.hecksagon +3 -15
  189. data/lib/hecks/language/bluebook/command.bluebook +27 -144
  190. data/lib/hecks/language/bluebook/entity.bluebook +24 -97
  191. data/lib/hecks/language/bluebook/policy.bluebook +16 -67
  192. data/lib/hecks/language/bluebook/process_manager.bluebook +28 -151
  193. data/lib/hecks/language/bluebook/projection.bluebook +126 -60
  194. data/lib/hecks/language/bluebook/query.bluebook +12 -43
  195. data/lib/hecks/language/bluebook/shape.bluebook +23 -73
  196. data/lib/hecks/language/bluebook/syntax.bluebook +51 -262
  197. data/lib/hecks/language/bluebook/vocabulary.bluebook +98 -197
  198. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +3 -10
  199. data/lib/hecks/language/hecksagon/domain_port.bluebook +4 -19
  200. data/lib/hecks/language/hecksagon/hecksagon.bluebook +6 -25
  201. data/lib/hecks/language/hecksagon/port_operation.bluebook +3 -14
  202. data/lib/hecks/language/oidc.json +35 -0
  203. data/lib/hecks/language/port.bluebook +4 -17
  204. data/lib/hecks/language/translation/translation.bluebook +3 -9
  205. data/lib/hecks/language/translation/translation_aggregate.bluebook +5 -24
  206. data/lib/hecks/language/world/wiring.bluebook +1 -3
  207. data/lib/hecks/language/world/world.bluebook +5 -7
  208. data/lib/hecks/literal.rb +8 -50
  209. data/lib/hecks/mcp_door_scope.rb +95 -0
  210. data/lib/hecks/mcp_stdio_guard.rb +4 -41
  211. data/lib/hecks/naming.rb +29 -137
  212. data/lib/hecks/ports/access_control.rb +7 -23
  213. data/lib/hecks/ports/agent/answers.rb +5 -23
  214. data/lib/hecks/ports/agent.rb +47 -172
  215. data/lib/hecks/ports/authentication.rb +12 -39
  216. data/lib/hecks/ports/authorization.rb +16 -43
  217. data/lib/hecks/ports/clock.rb +5 -45
  218. data/lib/hecks/ports/extraction.rb +7 -12
  219. data/lib/hecks/ports/identity_assignment.rb +8 -29
  220. data/lib/hecks/ports/identity_generation.rb +6 -29
  221. data/lib/hecks/ports/identity_resolution.rb +2 -10
  222. data/lib/hecks/ports/key_vault.rb +9 -45
  223. data/lib/hecks/ports/persistence/append_only.rb +62 -65
  224. data/lib/hecks/ports/persistence/binding_policy.rb +4 -14
  225. data/lib/hecks/ports/persistence/codec_boundary.rb +20 -80
  226. data/lib/hecks/ports/persistence/null_saga_store.rb +4 -20
  227. data/lib/hecks/ports/persistence/plugin.rb +13 -42
  228. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +38 -141
  229. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +15 -115
  230. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +16 -84
  231. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +7 -26
  232. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +3 -22
  233. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +55 -181
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/events.rb +77 -0
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +12 -122
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +28 -128
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +73 -474
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +31 -124
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +45 -219
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +13 -108
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +18 -75
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +9 -49
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +6 -123
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +23 -55
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +14 -72
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +7 -16
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +14 -115
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +13 -45
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +127 -554
  250. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +13 -63
  251. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +2 -17
  252. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +3 -14
  253. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +13 -58
  254. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -17
  255. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +9 -26
  256. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +9 -25
  257. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +22 -122
  258. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +34 -123
  259. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +5 -12
  260. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +3 -9
  261. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -14
  262. data/lib/hecks/ports/persistence/plugins/era.rb +4 -23
  263. data/lib/hecks/ports/persistence/postgres_dump/connection.rb +7 -23
  264. data/lib/hecks/ports/persistence/postgres_dump.rb +4 -35
  265. data/lib/hecks/ports/persistence/remote_runtime.rb +2 -24
  266. data/lib/hecks/ports/persistence/repository_factory.rb +8 -27
  267. data/lib/hecks/ports/persistence/state_codec.rb +23 -103
  268. data/lib/hecks/ports/persistence.rb +7 -14
  269. data/lib/hecks/ports/projection.rb +42 -84
  270. data/lib/hecks/ports/query/in_memory.rb +10 -58
  271. data/lib/hecks/ports/query/ordering.rb +11 -32
  272. data/lib/hecks/ports/query.rb +14 -37
  273. data/lib/hecks/ports.rb +2 -8
  274. data/lib/hecks/projections/bootstrap_table.rb +11 -43
  275. data/lib/hecks/projections/deploy/fargate/assembly.rb +2 -15
  276. data/lib/hecks/projections/deploy/fargate/cdn.rb +1 -48
  277. data/lib/hecks/projections/deploy/fargate/cdn_default.rb +2 -11
  278. data/lib/hecks/projections/deploy/fargate/check.rb +5 -14
  279. data/lib/hecks/projections/deploy/fargate/containers.rb +3 -78
  280. data/lib/hecks/projections/deploy/fargate/extras.rb +3 -44
  281. data/lib/hecks/projections/deploy/fargate/monitoring.rb +0 -44
  282. data/lib/hecks/projections/deploy/fargate/settings.rb +2 -54
  283. data/lib/hecks/projections/deploy/fargate/yaml.rb +13 -58
  284. data/lib/hecks/projections/deploy/fargate.rb +35 -202
  285. data/lib/hecks/projections/deploy/lambda.rb +151 -679
  286. data/lib/hecks/projections/deploy/preview/access_yaml.rb +1 -33
  287. data/lib/hecks/projections/deploy/preview/containers.rb +6 -111
  288. data/lib/hecks/projections/deploy/preview/routing_yaml.rb +4 -56
  289. data/lib/hecks/projections/deploy/preview/script.rb +2 -14
  290. data/lib/hecks/projections/deploy/preview/settings.rb +3 -33
  291. data/lib/hecks/projections/deploy/preview/task_yaml.rb +2 -38
  292. data/lib/hecks/projections/deploy/preview/template.rb +2 -58
  293. data/lib/hecks/projections/deploy/preview.rb +2 -67
  294. data/lib/hecks/projections/deploy/scripts/settings.rb +3 -25
  295. data/lib/hecks/projections/deploy/scripts.rb +4 -57
  296. data/lib/hecks/projections/deploy/shared.rb +47 -146
  297. data/lib/hecks/projections/deploy/smoke.rb +4 -61
  298. data/lib/hecks/projections/deploy/template_diff/comparer.rb +3 -25
  299. data/lib/hecks/projections/deploy/template_diff/loader.rb +2 -11
  300. data/lib/hecks/projections/deploy/template_diff.rb +5 -57
  301. data/lib/hecks/projections/diagrams.rb +20 -582
  302. data/lib/hecks/projections/glossary/html.rb +14 -130
  303. data/lib/hecks/projections/glossary/markdown.rb +8 -69
  304. data/lib/hecks/projections/glossary/mermaid.rb +9 -66
  305. data/lib/hecks/projections/glossary/sensitivity.rb +5 -34
  306. data/lib/hecks/projections/glossary/sentences.rb +14 -159
  307. data/lib/hecks/projections/glossary.rb +30 -222
  308. data/lib/hecks/projections/ir.rb +3 -10
  309. data/lib/hecks/projections/model/deviations.rb +15 -42
  310. data/lib/hecks/projections/model.rb +12 -51
  311. data/lib/hecks/projections/oidc.rb +12 -56
  312. data/lib/hecks/projections/parser_table.rb +9 -28
  313. data/lib/hecks/projections/reference.rb +9 -16
  314. data/lib/hecks/projections/rust_vocabulary.rb +12 -250
  315. data/lib/hecks/projections/shape.rb +3 -29
  316. data/lib/hecks/projections/statements.rb +7 -86
  317. data/lib/hecks/projections/vocabulary.rb +6 -22
  318. data/lib/hecks/projections.rb +2 -16
  319. data/lib/hecks/projector/cli_projector.rb +60 -375
  320. data/lib/hecks/projector/docs_projector.rb +10 -201
  321. data/lib/hecks/projector/exporter.rb +46 -138
  322. data/lib/hecks/projector/ir_projector.rb +2 -9
  323. data/lib/hecks/projector/narrate_projector.rb +15 -189
  324. data/lib/hecks/projector/target.rb +13 -76
  325. data/lib/hecks/projector.rb +30 -121
  326. data/lib/hecks/query_specification/common/comparators.rb +5 -26
  327. data/lib/hecks/query_specification/common/comparison.rb +35 -143
  328. data/lib/hecks/query_specification/common/dsl.rb +8 -24
  329. data/lib/hecks/query_specification/common/null_policy.rb +20 -70
  330. data/lib/hecks/query_specification/common/options.rb +2 -6
  331. data/lib/hecks/query_specification/common/where_clause.rb +2 -10
  332. data/lib/hecks/query_specification/field_path.rb +37 -77
  333. data/lib/hecks/query_specification/hop_path.rb +12 -76
  334. data/lib/hecks/release/gem_pin.rb +2 -26
  335. data/lib/hecks/release/runner/ci_publisher.rb +93 -0
  336. data/lib/hecks/release/runner/commands.rb +66 -0
  337. data/lib/hecks/release/runner/console.rb +52 -0
  338. data/lib/hecks/release/runner/gem_publisher.rb +43 -0
  339. data/lib/hecks/release/runner/git.rb +48 -0
  340. data/lib/hecks/release/runner/npm_publisher.rb +75 -0
  341. data/lib/hecks/release/runner/preflight.rb +104 -0
  342. data/lib/hecks/release/runner/published.rb +55 -0
  343. data/lib/hecks/release/runner/tagger.rb +80 -0
  344. data/lib/hecks/release/runner.rb +215 -0
  345. data/lib/hecks/rendering.rb +4 -26
  346. data/lib/hecks/router/namespace_installer.rb +12 -36
  347. data/lib/hecks/router.rb +4 -8
  348. data/lib/hecks/runtime/aggregate_lock.rb +8 -33
  349. data/lib/hecks/runtime/boot_gates.rb +3 -18
  350. data/lib/hecks/runtime/caller.rb +19 -59
  351. data/lib/hecks/runtime/capability_graph.rb +5 -21
  352. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +16 -78
  353. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +23 -160
  354. data/lib/hecks/runtime/command_interpreter.rb +125 -307
  355. data/lib/hecks/runtime/command_rules/admissibility.rb +95 -366
  356. data/lib/hecks/runtime/command_rules/arithmetic.rb +66 -196
  357. data/lib/hecks/runtime/command_rules/authorization.rb +13 -45
  358. data/lib/hecks/runtime/command_rules/emission.rb +8 -21
  359. data/lib/hecks/runtime/command_rules/references.rb +23 -202
  360. data/lib/hecks/runtime/dependency_planning.rb +30 -102
  361. data/lib/hecks/runtime/dispatcher.rb +82 -241
  362. data/lib/hecks/runtime/entity_element.rb +40 -462
  363. data/lib/hecks/runtime/entity_interpreter.rb +65 -220
  364. data/lib/hecks/runtime/errors.rb +32 -115
  365. data/lib/hecks/runtime/event.rb +3 -25
  366. data/lib/hecks/runtime/identity.rb +18 -154
  367. data/lib/hecks/runtime/instance.rb +18 -150
  368. data/lib/hecks/runtime/interpreting.rb +11 -70
  369. data/lib/hecks/runtime/invocation.rb +28 -164
  370. data/lib/hecks/runtime/loader.rb +48 -186
  371. data/lib/hecks/runtime/outbox.rb +38 -261
  372. data/lib/hecks/runtime/policy_interpreter.rb +38 -200
  373. data/lib/hecks/runtime/port_operation_interpreter.rb +26 -87
  374. data/lib/hecks/runtime/query_interpreter.rb +45 -187
  375. data/lib/hecks/runtime/reaction_invocation.rb +36 -138
  376. data/lib/hecks/runtime/read_model_interpreter.rb +139 -208
  377. data/lib/hecks/runtime/rebuild_sweep.rb +8 -54
  378. data/lib/hecks/runtime/reference_hop.rb +8 -81
  379. data/lib/hecks/runtime/refusal_wording.rb +8 -89
  380. data/lib/hecks/runtime/registry/saga_persistence.rb +10 -102
  381. data/lib/hecks/runtime/registry/verification.rb +52 -225
  382. data/lib/hecks/runtime/registry/world_defaults.rb +4 -53
  383. data/lib/hecks/runtime/registry.rb +103 -244
  384. data/lib/hecks/runtime/remote_dispatcher.rb +37 -152
  385. data/lib/hecks/runtime/routing.rb +4 -15
  386. data/lib/hecks/runtime/saga_interpreter/correlation.rb +14 -61
  387. data/lib/hecks/runtime/saga_interpreter.rb +79 -263
  388. data/lib/hecks/runtime/saga_pending_dispatch.rb +2 -43
  389. data/lib/hecks/runtime/tenant_check.rb +6 -68
  390. data/lib/hecks/runtime/tenant_scope.rb +3 -28
  391. data/lib/hecks/runtime/value/admission.rb +16 -67
  392. data/lib/hecks/runtime/value/coercion.rb +155 -641
  393. data/lib/hecks/runtime/value/entity_list_coercion.rb +26 -219
  394. data/lib/hecks/runtime/value.rb +12 -84
  395. data/lib/hecks/runtime.rb +17 -65
  396. data/lib/hecks/storehouse.rb +146 -567
  397. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +8 -32
  398. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +6 -23
  399. data/lib/hecks/vendoring/git_environment.rb +2 -8
  400. data/lib/hecks/vendoring/git_source.rb +4 -10
  401. data/lib/hecks/vendoring.rb +7 -46
  402. data/lib/hecks/version.rb +3 -14
  403. data/lib/hecks/vocabulary.rb +5 -0
  404. data/lib/hecks.rb +32 -82
  405. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -46
  406. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +11 -80
  407. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +10 -99
  408. metadata +30 -59
  409. data/lib/hecks/bench/cli.rb +0 -74
  410. data/lib/hecks/bench/environment.rb +0 -100
  411. data/lib/hecks/bench/postgres_probe.rb +0 -41
  412. data/lib/hecks/bench/report.rb +0 -116
  413. data/lib/hecks/bench/ruby_runner.rb +0 -151
  414. data/lib/hecks/bench/run.rb +0 -55
  415. data/lib/hecks/bench/rust_runner.rb +0 -165
  416. data/lib/hecks/bench/stats.rb +0 -75
  417. data/lib/hecks/bench/suite.rb +0 -172
  418. data/lib/hecks/bench/workload.rb +0 -137
  419. data/lib/hecks/bench.rb +0 -28
  420. data/lib/hecks/codemod.rb +0 -443
  421. data/lib/hecks/corpus.rb +0 -479
  422. data/lib/hecks/doc/reference.rb +0 -585
  423. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -684
  424. data/lib/hecks/fuzzing/combination_miner.rb +0 -178
  425. data/lib/hecks/fuzzing/concurrent_dispatch.rb +0 -469
  426. data/lib/hecks/fuzzing/coverage_campaign.rb +0 -161
  427. data/lib/hecks/fuzzing/differential.rb +0 -192
  428. data/lib/hecks/fuzzing/domain_generator.rb +0 -871
  429. data/lib/hecks/fuzzing/era_boundary.rb +0 -154
  430. data/lib/hecks/fuzzing/form_census.rb +0 -285
  431. data/lib/hecks/fuzzing/generated_domain_check.rb +0 -171
  432. data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -125
  433. data/lib/hecks/fuzzing/isolated_boot.rb +0 -658
  434. data/lib/hecks/fuzzing/nondeterministic.rb +0 -79
  435. data/lib/hecks/fuzzing/persistence_parity.rb +0 -253
  436. data/lib/hecks/fuzzing/properties/corrections.rb +0 -125
  437. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +0 -688
  438. data/lib/hecks/fuzzing/properties/guards.rb +0 -398
  439. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +0 -399
  440. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +0 -137
  441. data/lib/hecks/fuzzing/properties/outbox.rb +0 -180
  442. data/lib/hecks/fuzzing/properties/querying.rb +0 -260
  443. data/lib/hecks/fuzzing/properties.rb +0 -267
  444. data/lib/hecks/fuzzing/qa_settings.rb +0 -164
  445. data/lib/hecks/fuzzing/replay.rb +0 -930
  446. data/lib/hecks/fuzzing/rotation_priority.rb +0 -114
  447. data/lib/hecks/fuzzing/rust_gap_manifest.rb +0 -139
  448. data/lib/hecks/fuzzing/self_consistency.rb +0 -825
  449. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +0 -532
  450. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +0 -171
  451. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +0 -127
  452. data/lib/hecks/fuzzing/sequence_generator/picker.rb +0 -116
  453. data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +0 -138
  454. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +0 -344
  455. data/lib/hecks/fuzzing/sequence_generator.rb +0 -311
  456. data/lib/hecks/fuzzing/shrinker.rb +0 -309
  457. data/lib/hecks/fuzzing/structural_skips.rb +0 -53
  458. data/lib/hecks/fuzzing/sweep_depth.rb +0 -61
  459. data/lib/hecks/fuzzing/target_capabilities.rb +0 -255
  460. data/lib/hecks/fuzzing/value_generator.rb +0 -340
  461. data/lib/hecks/fuzzing.rb +0 -23
  462. data/lib/hecks/grammar/evolve.rb +0 -524
  463. data/lib/hecks/query_ir.rb +0 -482
@@ -1,151 +0,0 @@
1
- require_relative "run"
2
-
3
- module Hecks
4
- module Bench
5
- # Measures the Ruby runtime, in-process, on one persistence adapter.
6
- #
7
- # The domain is booted from a throwaway copy through `Fuzzing::IsolatedBoot`, so a
8
- # run always starts from an empty store and never touches the example's own data.
9
- # That is the same isolation `bin/fuzz --adapter` uses. Both Postgres adapters write
10
- # to a uniquely named schema in the `hecks_bench` database, which is dropped after the
11
- # run, so a concurrent `bin/fuzz` and its shared `hecks_fuzz` schema are never touched.
12
- #
13
- # Only `dispatch_flat` is inside the timer. Boot, setup commands and warmup are not.
14
- module RubyRunner
15
- # The adapters this runner can bind, as `IsolatedBoot` names them.
16
- ADAPTERS = %i[memory sqlite postgres postgres_era].freeze
17
-
18
- # The adapters that need a server and a scratch schema.
19
- POSTGRES_ADAPTERS = %i[postgres postgres_era].freeze
20
-
21
- # The scratch database both Postgres adapters run in.
22
- DATABASE = "hecks_bench".freeze
23
-
24
- module_function
25
-
26
- # Boots the workload's domain on `adapter` and times its commands.
27
- #
28
- # @param workload [Workload] the commands to dispatch
29
- # @param adapter [Symbol] one of `ADAPTERS`
30
- # @param warmup [Integer] cycles dispatched and discarded before timing starts
31
- # @param iterations [Integer] cycles timed
32
- # @return [Run] the timings of the measured cycles
33
- # @raise [ArgumentError] if `adapter` is not one of `ADAPTERS`
34
- # @raise [StandardError] whatever refusal a command raises, warmup included, since a
35
- # benchmark that swallowed one would be timing something other than its workload
36
- def call(workload, adapter:, warmup:, iterations:)
37
- unless ADAPTERS.include?(adapter)
38
- raise ArgumentError, "unknown adapter #{adapter.inspect} — one of #{ADAPTERS.join(', ')}"
39
- end
40
-
41
- load_dependencies
42
- schema = "bench_#{Process.pid}_#{rand(1 << 32).to_s(16)}"
43
- quietly_for_postgres do
44
- boot(workload, adapter, schema) do |runtime|
45
- workload.setup.each { |step| dispatch(runtime, step) }
46
- measure(runtime, workload, warmup: warmup, iterations: iterations)
47
- end
48
- end
49
- ensure
50
- drop_schema(schema) if schema && POSTGRES_ADAPTERS.include?(adapter)
51
- end
52
-
53
- # Boots the workload's domain on `adapter` in an isolated copy.
54
- #
55
- # @param workload [Workload] the domain to boot
56
- # @param adapter [Symbol] one of `ADAPTERS`
57
- # @param schema [String] the scratch schema name, used only by the Postgres adapters
58
- # @yield [Hecks::Runtime] the booted domain
59
- # @return [Object] whatever the block returns
60
- def boot(workload, adapter, schema)
61
- Hecks::Fuzzing::IsolatedBoot.call(workload.domain_path, adapter: adapter, database: DATABASE,
62
- schema: schema, scratch: { database: DATABASE, schema: schema }) do |copy|
63
- yield Hecks.boot(copy, install_facade: false)
64
- end
65
- end
66
-
67
- # Keeps Postgres's `schema already exists` notices off stderr while a block runs.
68
- #
69
- # Every aggregate opens its own connection and each one repeats the notice, which
70
- # buries the progress lines. The server setting is restored afterwards.
71
- #
72
- # @yield the work to run with notices quieted
73
- # @return [Object] whatever the block returns
74
- def quietly_for_postgres
75
- previous = ENV.fetch("PGOPTIONS", nil)
76
- ENV["PGOPTIONS"] = [previous, "-c client_min_messages=warning"].compact.join(" ")
77
- yield
78
- ensure
79
- ENV["PGOPTIONS"] = previous
80
- end
81
-
82
- # Dispatches warmup cycles untimed, then times the measured ones.
83
- #
84
- # @param runtime [Hecks::Runtime] the booted domain
85
- # @param workload [Workload] the commands to dispatch
86
- # @param warmup [Integer] cycles to discard
87
- # @param iterations [Integer] cycles to time
88
- # @return [Run] the timings of the measured cycles
89
- def measure(runtime, workload, warmup:, iterations:)
90
- warmup.times { |n| workload.cycle(n).each { |step| dispatch(runtime, step) } }
91
- GC.start
92
- samples = []
93
- started = now
94
- iterations.times do |n|
95
- workload.cycle(warmup + n).each { |step| samples << [step.verb, time { dispatch(runtime, step) }] }
96
- end
97
- Run.new(samples: samples, wall_seconds: now - started)
98
- end
99
-
100
- # Sends one step to the runtime.
101
- #
102
- # @param runtime [Hecks::Runtime] the booted domain
103
- # @param step [Workload::Step] the command to dispatch
104
- # @return [Object] whatever `dispatch_flat` answers
105
- def dispatch(runtime, step)
106
- runtime.dispatch_flat(step.verb, step.ruby_args)
107
- end
108
-
109
- # Reads the monotonic clock.
110
- #
111
- # @return [Float] seconds on the monotonic clock
112
- def now
113
- Process.clock_gettime(Process::CLOCK_MONOTONIC)
114
- end
115
-
116
- # Times a block.
117
- #
118
- # @yield the work to time
119
- # @return [Float] seconds the block took
120
- def time
121
- started = now
122
- yield
123
- now - started
124
- end
125
-
126
- # Loads what booting a domain on every adapter needs.
127
- #
128
- # @return [void]
129
- def load_dependencies
130
- require "hecks"
131
- require "hecks/fuzzing"
132
- require "hecks/ports/persistence/plugins/era"
133
- end
134
-
135
- # Removes the schema a Postgres run created, so repeated runs do not accumulate.
136
- #
137
- # @param schema [String] the schema name this run used
138
- # @return [void]
139
- def drop_schema(schema)
140
- require "pg"
141
- connection = PG.connect(dbname: DATABASE, connect_timeout: 2)
142
- connection.exec("SET client_min_messages = warning")
143
- connection.exec("DROP SCHEMA IF EXISTS #{connection.quote_ident(schema)} CASCADE")
144
- rescue LoadError, PG::Error
145
- nil
146
- ensure
147
- connection&.close
148
- end
149
- end
150
- end
151
- end
@@ -1,55 +0,0 @@
1
- module Hecks
2
- module Bench
3
- # The raw timings of one measured pass over a workload on one runtime.
4
- #
5
- # Runners produce these and know nothing about reporting. {#summary} is the only
6
- # place raw samples become the figures a report prints.
7
- class Run
8
- # @return [Array<Array(String, Float)>] each timed command's verb and latency in
9
- # seconds, in dispatch order
10
- attr_reader :samples
11
-
12
- # @return [Float] seconds from the first timed command's start to the last one's end
13
- attr_reader :wall_seconds
14
-
15
- # @return [Hash{Symbol => Object}] facts a runner wants reported alongside the timings
16
- attr_reader :extras
17
-
18
- # Builds a run.
19
- #
20
- # @param samples [Array<Array(String, Float)>] verb and latency seconds per command
21
- # @param wall_seconds [Float] the wall-clock duration of the whole timed pass
22
- # @param extras [Hash{Symbol => Object}] runner-specific facts, such as a round-trip floor
23
- def initialize(samples:, wall_seconds:, extras: {})
24
- @samples = samples
25
- @wall_seconds = wall_seconds
26
- @extras = extras
27
- end
28
-
29
- # Reduces the samples to the figures a report prints.
30
- #
31
- # @return [Hash{Symbol => Object}] `:commands`, `:wall_seconds`, `:throughput_per_s`,
32
- # the `Stats.latency` figures, `:drift`, `:by_verb` (each verb's count, p50 and p99),
33
- # and any `extras`
34
- def summary
35
- seconds = samples.map(&:last)
36
- {
37
- commands: samples.size,
38
- wall_seconds: wall_seconds.round(4),
39
- throughput_per_s: (samples.size / wall_seconds).round(1),
40
- **Stats.latency(seconds),
41
- drift: Stats.drift(seconds),
42
- by_verb: by_verb
43
- }.merge(extras)
44
- end
45
-
46
- private
47
-
48
- def by_verb
49
- samples.group_by(&:first).transform_values do |group|
50
- Stats.latency(group.map(&:last)).slice(:count, :p50_us, :p99_us)
51
- end
52
- end
53
- end
54
- end
55
- end
@@ -1,165 +0,0 @@
1
- require "fileutils"
2
- require "open3"
3
- require_relative "run"
4
-
5
- module Hecks
6
- module Bench
7
- # Measures the native Rust binary through its streaming mode.
8
- #
9
- # `rust --serve` reads one JSON step per line and answers one JSON line each, keeping
10
- # its in-memory store alive between them. This runner writes a step, waits for the
11
- # answer line, and times the whole round trip, so the figures include two pipe
12
- # crossings and the binary's own JSON parse and print. Process start-up is outside
13
- # the timer.
14
- #
15
- # Alongside each run it measures a **round-trip floor**: the same round trip for a
16
- # step the binary refuses immediately, which is what the pipes cost with almost no
17
- # domain work behind them.
18
- module RustRunner
19
- # Where the Rust crate lives.
20
- RUST_DIR = File.expand_path("../../../rust", __dir__)
21
-
22
- # How many refused-at-once steps make up the round-trip floor's sample.
23
- FLOOR_SAMPLES = 500
24
-
25
- # A `cargo build` that exited non-zero.
26
- class BuildFailed < StandardError; end
27
-
28
- # A benchmarked step the binary refused or could not answer.
29
- class StepRefused < StandardError; end
30
-
31
- module_function
32
-
33
- # Says why the Rust target cannot run, if it cannot.
34
- #
35
- # @param binary [String, nil] an explicit pre-built binary, which needs no toolchain
36
- # @return [String, nil] nil when the target can run, otherwise one sentence saying why
37
- def unavailable_reason(binary: nil)
38
- return "the binary #{binary} does not exist or is not executable" if binary && !File.executable?(binary)
39
- return nil if binary || Environment.capture("cargo", "-V") != "unknown"
40
-
41
- "`cargo` is not on PATH and no --rust-binary was given"
42
- end
43
-
44
- # Builds the release binary for one domain's Cargo feature and pins a copy of it.
45
- #
46
- # The copy has its own name so building another domain's feature afterwards cannot
47
- # replace it.
48
- #
49
- # @param feature [String] the domain's Cargo feature, its directory name
50
- # @param rust_dir [String] the Rust crate's directory
51
- # @return [String] the path of the pinned release binary
52
- # @raise [BuildFailed] if cargo exits non-zero or leaves no binary
53
- def build(feature, rust_dir: RUST_DIR)
54
- output, status = Open3.capture2e("cargo", "build", "--release", "--no-default-features",
55
- "--features", feature, chdir: rust_dir)
56
- unless status.success?
57
- raise BuildFailed,
58
- "cargo build --release --features #{feature} failed:\n#{output.lines.last(15).join}"
59
- end
60
-
61
- target = File.join(ENV.fetch("CARGO_TARGET_DIR", File.join(rust_dir, "target")), "release")
62
- pinned = File.join(target, "rust-bench-#{feature}")
63
- FileUtils.cp(File.join(target, "rust"), pinned)
64
- pinned
65
- end
66
-
67
- # Starts the binary in serve mode and times the workload's commands.
68
- #
69
- # @param workload [Workload] the commands to send
70
- # @param binary [String] the path of a binary built for `workload`'s domain
71
- # @param warmup [Integer] cycles sent and discarded before timing starts
72
- # @param iterations [Integer] cycles timed
73
- # @return [Run] the timings, with the round-trip floor in its extras as
74
- # `:roundtrip_floor_p50_us`
75
- # @raise [StepRefused] if the binary refuses any step, warmup included
76
- def call(workload, binary:, warmup:, iterations:)
77
- IO.popen([binary, "--serve"], "r+") do |pipe|
78
- pipe.sync = true
79
- workload.setup.each { |step| ask(pipe, step.to_json_line) }
80
- warmup.times { |n| workload.cycle(n).each { |step| ask(pipe, step.to_json_line) } }
81
- run = measure(pipe, workload, warmup: warmup, iterations: iterations)
82
- Run.new(samples: run.samples, wall_seconds: run.wall_seconds,
83
- extras: { roundtrip_floor_p50_us: floor(pipe) })
84
- ensure
85
- pipe.close_write unless pipe.closed?
86
- end
87
- end
88
-
89
- # Times the measured cycles.
90
- #
91
- # @param pipe [IO] the binary's stdin and stdout
92
- # @param workload [Workload] the commands to send
93
- # @param warmup [Integer] cycles already sent, so record names continue from there
94
- # @param iterations [Integer] cycles to time
95
- # @return [Run] the timings, without extras
96
- def measure(pipe, workload, warmup:, iterations:)
97
- samples = []
98
- started = now
99
- iterations.times do |n|
100
- workload.cycle(warmup + n).each do |step|
101
- line = step.to_json_line
102
- began = now
103
- answer = round_trip(pipe, line)
104
- samples << [step.verb, now - began]
105
- check(answer, step.verb)
106
- end
107
- end
108
- Run.new(samples: samples, wall_seconds: now - started)
109
- end
110
-
111
- # Measures the pipes with almost no work behind them.
112
- #
113
- # @param pipe [IO] the binary's stdin and stdout
114
- # @return [Float] the p50 microseconds of {FLOOR_SAMPLES} round trips for an empty step,
115
- # which the binary refuses without touching its store
116
- def floor(pipe)
117
- seconds = Array.new(FLOOR_SAMPLES) do
118
- began = now
119
- round_trip(pipe, "{}")
120
- now - began
121
- end
122
- Stats.micros(Stats.percentile(seconds, 0.5))
123
- end
124
-
125
- # Sends one step and returns the answer line, untimed.
126
- #
127
- # @param pipe [IO] the binary's stdin and stdout
128
- # @param line [String] the step as one JSON line
129
- # @return [void]
130
- # @raise [StepRefused] if the binary refuses the step
131
- def ask(pipe, line)
132
- check(round_trip(pipe, line), line[0, 80])
133
- end
134
-
135
- # Writes one line and reads one line back.
136
- #
137
- # @param pipe [IO] the binary's stdin and stdout
138
- # @param line [String] the step as one JSON line
139
- # @return [String] the answer line, or an empty string if the binary closed its output
140
- def round_trip(pipe, line)
141
- pipe.write(line, "\n")
142
- pipe.gets.to_s
143
- end
144
-
145
- # Refuses an answer that is not a success.
146
- #
147
- # @param answer [String] the binary's answer line
148
- # @param what [String] what was asked, for the error message
149
- # @return [void]
150
- # @raise [StepRefused] unless `answer` starts with `{"ok":true`
151
- def check(answer, what)
152
- return if answer.start_with?('{"ok":true')
153
-
154
- raise StepRefused, "#{what} was not accepted: #{answer.strip.empty? ? 'the binary closed its output' : answer.strip}"
155
- end
156
-
157
- # Reads the monotonic clock.
158
- #
159
- # @return [Float] seconds on the monotonic clock
160
- def now
161
- Process.clock_gettime(Process::CLOCK_MONOTONIC)
162
- end
163
- end
164
- end
165
- end
@@ -1,75 +0,0 @@
1
- module Hecks
2
- module Bench
3
- # Order statistics over a list of latency samples.
4
- #
5
- # Percentiles use the nearest-rank method: the smallest sample with at least
6
- # that fraction of the samples at or below it. It never interpolates, so every
7
- # reported figure is a latency that actually happened. With fewer than 100
8
- # samples the p99 is simply the maximum.
9
- module Stats
10
- module_function
11
-
12
- # Reads one percentile from a list of samples.
13
- #
14
- # @param samples [Array<Float>] the samples, in any order
15
- # @param fraction [Float] the percentile as a fraction, e.g. `0.99`
16
- # @return [Float, nil] the nearest-rank percentile, or nil when `samples` is empty
17
- def percentile(samples, fraction)
18
- return nil if samples.empty?
19
-
20
- sorted = samples.sort
21
- sorted[[(fraction * sorted.size).ceil - 1, 0].max]
22
- end
23
-
24
- # Finds the middle value of a list, averaging the two middle values of an even one.
25
- #
26
- # @param values [Array<Numeric>] the values, in any order
27
- # @return [Float, nil] the median, or nil when `values` is empty
28
- def median(values)
29
- return nil if values.empty?
30
-
31
- sorted = values.sort
32
- middle = sorted.size / 2
33
- sorted.size.odd? ? sorted[middle].to_f : (sorted[middle - 1] + sorted[middle]) / 2.0
34
- end
35
-
36
- # Summarizes one run's latency samples.
37
- #
38
- # @param seconds [Array<Float>] per-command latencies in seconds, in dispatch order
39
- # @return [Hash{Symbol => Numeric}] `:count`, and `:p50_us`, `:p99_us`, `:mean_us` and
40
- # `:max_us` in microseconds
41
- def latency(seconds)
42
- {
43
- count: seconds.size,
44
- p50_us: micros(percentile(seconds, 0.50)),
45
- p99_us: micros(percentile(seconds, 0.99)),
46
- mean_us: micros(seconds.sum / seconds.size),
47
- max_us: micros(seconds.max)
48
- }
49
- end
50
-
51
- # Compares how slow the last tenth of a run was against its first tenth.
52
- #
53
- # A ratio well above 1.0 means latency grows as the store fills, which a single
54
- # p50 over the whole run would hide.
55
- #
56
- # @param seconds [Array<Float>] per-command latencies in dispatch order
57
- # @return [Float, nil] the last tenth's p50 over the first tenth's, or nil when there
58
- # are fewer than ten samples
59
- def drift(seconds)
60
- tenth = seconds.size / 10
61
- return nil if tenth.zero?
62
-
63
- (percentile(seconds.last(tenth), 0.5) / percentile(seconds.first(tenth), 0.5)).round(2)
64
- end
65
-
66
- # Converts seconds to microseconds, rounded to a tenth.
67
- #
68
- # @param seconds [Float, nil] a duration in seconds
69
- # @return [Float, nil] the duration in microseconds, or nil when `seconds` is nil
70
- def micros(seconds)
71
- seconds && (seconds * 1_000_000).round(1)
72
- end
73
- end
74
- end
75
- end
@@ -1,172 +0,0 @@
1
- module Hecks
2
- module Bench
3
- # Runs every requested target against every requested domain and aggregates the runs.
4
- #
5
- # Each domain's runs are interleaved across targets (memory, sqlite, ..., memory,
6
- # sqlite, ...) rather than finished one target at a time, so a burst of unrelated load on
7
- # the machine lands on every target instead of flattering one of them. Each figure in the
8
- # result is the median across runs, with the fastest and slowest throughput kept so a noisy
9
- # measurement shows.
10
- module Suite
11
- # Every target `bin/bench` can measure.
12
- TARGETS = %w[ruby:memory ruby:sqlite ruby:postgres ruby:postgres_era rust].freeze
13
-
14
- # What to measure and how hard.
15
- #
16
- # @!attribute domains
17
- # @return [Array<String>] workload names, keys of `Workload.all`
18
- # @!attribute targets
19
- # @return [Array<String>] members of `TARGETS`
20
- # @!attribute warmup
21
- # @return [Integer] cycles discarded before each run is timed
22
- # @!attribute iterations
23
- # @return [Integer] cycles timed in each run
24
- # @!attribute runs
25
- # @return [Integer] how many fresh boots of each target to measure per domain
26
- # @!attribute rust_binary
27
- # @return [String, nil] a pre-built binary to use instead of building one; only valid
28
- # with a single domain, since a binary embeds exactly one
29
- Config = Struct.new(:domains, :targets, :warmup, :iterations, :runs, :rust_binary, keyword_init: true)
30
-
31
- module_function
32
-
33
- # Measures everything `config` asks for.
34
- #
35
- # @param config [Config] what to measure
36
- # @param log [#puts] where progress lines go; one line per run
37
- # @return [Hash{Symbol => Object}] `:environment`, `:config`, `:results` (one entry per
38
- # domain and target, see `.aggregate`) and `:skipped` (each skipped target and why)
39
- # @raise [ArgumentError] if a target or domain is unknown, a count is not positive, or
40
- # `rust_binary` is combined with more than one domain
41
- def call(config, log: $stderr)
42
- validate(config)
43
- load_before = Environment.load_average
44
- skipped = unavailable_targets(config)
45
- active = config.targets - skipped.map { |entry| entry[:target] }
46
- skipped.each { |entry| log.puts "skip #{entry[:target]}: #{entry[:reason]}" }
47
- results = config.domains.flat_map { |domain| measure_domain(Workload.fetch(domain), active, config, log) }
48
- environment = Environment.describe
49
- environment[:load_average] = { before: load_before, after: environment[:load_average] }
50
- { environment: environment.merge(postgres: postgres_version(active)),
51
- config: config.to_h, results: results, skipped: skipped }
52
- end
53
-
54
- # Checks a configuration before anything is booted.
55
- #
56
- # @param config [Config] the configuration to check
57
- # @return [void]
58
- # @raise [ArgumentError] on the first problem found
59
- def validate(config)
60
- unknown = config.targets - TARGETS
61
- raise ArgumentError, "unknown target #{unknown.first.inspect} — one of #{TARGETS.join(', ')}" if unknown.any?
62
-
63
- config.domains.each { |domain| Workload.fetch(domain) }
64
- if config.iterations < 1 || config.runs < 1 || config.warmup.negative?
65
- raise ArgumentError, "iterations and runs must be at least 1, and warmup at least 0"
66
- end
67
- return unless config.rust_binary && config.domains.size > 1
68
-
69
- raise ArgumentError, "--rust-binary embeds one domain, so it needs exactly one --domain"
70
- end
71
-
72
- # Finds the targets this machine cannot run.
73
- #
74
- # @param config [Config] the configuration whose targets to check
75
- # @return [Array<Hash{Symbol => String}>] `{target:, reason:}` for each unavailable target
76
- def unavailable_targets(config)
77
- postgres = PostgresProbe.unavailable_reason if config.targets.any? { |t| t.start_with?("ruby:postgres") }
78
- rust = RustRunner.unavailable_reason(binary: config.rust_binary) if config.targets.include?("rust")
79
- config.targets.filter_map do |target|
80
- reason = target == "rust" ? rust : (postgres if target.start_with?("ruby:postgres"))
81
- { target: target, reason: reason } if reason
82
- end
83
- end
84
-
85
- # Measures one domain across every active target, `config.runs` times each.
86
- #
87
- # @param workload [Workload] the domain's commands
88
- # @param targets [Array<String>] the targets that can run here
89
- # @param config [Config] warmup, iterations, runs and any binary override
90
- # @param log [#puts] where progress lines go
91
- # @return [Array<Hash>] one aggregated entry per target
92
- def measure_domain(workload, targets, config, log)
93
- binaries = {}
94
- summaries = Hash.new { |hash, target| hash[target] = [] }
95
- config.runs.times do |index|
96
- targets.each do |target|
97
- run = measure_once(workload, target, config, binaries)
98
- summaries[target] << run.summary
99
- log.puts progress_line("#{workload.name} #{target}", "run #{index + 1}/#{config.runs}", summaries[target].last)
100
- end
101
- end
102
- targets.map { |target| aggregate(workload.name, target, summaries[target]) }
103
- end
104
-
105
- # Formats the one line printed as each run finishes.
106
- #
107
- # @param label [String] the domain and target
108
- # @param position [String] which run of how many, e.g. `"2/3"`
109
- # @param summary [Hash] the run's `Run#summary`
110
- # @return [String] the progress line
111
- def progress_line(label, position, summary)
112
- "#{label.ljust(27)} #{position} #{summary[:throughput_per_s].round.to_s.rjust(8)} cmds/s " \
113
- "p50 #{summary[:p50_us].to_s.rjust(9)} us p99 #{summary[:p99_us].to_s.rjust(9)} us"
114
- end
115
-
116
- # Runs one target once.
117
- #
118
- # @param workload [Workload] the domain's commands
119
- # @param target [String] a member of `TARGETS`
120
- # @param config [Config] warmup, iterations and any binary override
121
- # @param binaries [Hash{String => String}] Rust binaries already built, by domain name;
122
- # filled in as they are built
123
- # @return [Run] the run's timings
124
- def measure_once(workload, target, config, binaries)
125
- if target == "rust"
126
- binary = config.rust_binary || (binaries[workload.name] ||= RustRunner.build(workload.name))
127
- RustRunner.call(workload, binary: binary, warmup: config.warmup, iterations: config.iterations)
128
- else
129
- RubyRunner.call(workload, adapter: target.delete_prefix("ruby:").to_sym,
130
- warmup: config.warmup, iterations: config.iterations)
131
- end
132
- end
133
-
134
- # Reduces one target's runs to a single entry.
135
- #
136
- # @param domain [String] the workload name
137
- # @param target [String] the target name
138
- # @param summaries [Array<Hash>] each run's `Run#summary`
139
- # @return [Hash{Symbol => Object}] `:domain`, `:target`, `:runs` (the raw summaries),
140
- # `:median` (the median of each run's throughput, p50, p99 and drift, plus
141
- # `:by_verb`), and `:throughput_range` (`[slowest, fastest]` across runs)
142
- def aggregate(domain, target, summaries)
143
- rates = summaries.map { |summary| summary[:throughput_per_s] }
144
- median = %i[throughput_per_s p50_us p99_us mean_us max_us drift].to_h do |key|
145
- [key, Stats.median(summaries.filter_map { |summary| summary[key] })]
146
- end
147
- median[:roundtrip_floor_p50_us] = Stats.median(summaries.filter_map { |s| s[:roundtrip_floor_p50_us] })
148
- median[:by_verb] = median_by_verb(summaries)
149
- { domain: domain, target: target, runs: summaries, median: median.compact,
150
- throughput_range: rates.minmax }
151
- end
152
-
153
- # Takes each verb's median p50 and p99 across runs.
154
- #
155
- # @param summaries [Array<Hash>] each run's `Run#summary`
156
- # @return [Hash{String => Hash{Symbol => Float}}] `:p50_us` and `:p99_us` per verb
157
- def median_by_verb(summaries)
158
- summaries.first[:by_verb].keys.to_h do |verb|
159
- [verb, %i[p50_us p99_us].to_h { |key| [key, Stats.median(summaries.map { |s| s[:by_verb][verb][key] })] }]
160
- end
161
- end
162
-
163
- # Reads the Postgres server version if a Postgres target ran.
164
- #
165
- # @param active [Array<String>] the targets that ran
166
- # @return [String, nil] the server version, or nil when no Postgres target ran
167
- def postgres_version(active)
168
- PostgresProbe.server_version if active.any? { |target| target.start_with?("ruby:postgres") }
169
- end
170
- end
171
- end
172
- end