hecks 0.2.0 → 1.0.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 (419) hide show
  1. checksums.yaml +5 -5
  2. data/lib/hecks/adapters/driven/claude_code.adapter +3 -0
  3. data/lib/hecks/adapters/driven/claude_code.rb +127 -0
  4. data/lib/hecks/adapters/driven/d1.adapter +12 -0
  5. data/lib/hecks/adapters/driven/d1.rb +427 -0
  6. data/lib/hecks/adapters/driven/folder.adapter +3 -0
  7. data/lib/hecks/adapters/driven/folder.rb +199 -0
  8. data/lib/hecks/adapters/driven/google_authentication.adapter +3 -0
  9. data/lib/hecks/adapters/driven/google_authentication.rb +100 -0
  10. data/lib/hecks/adapters/driven/governance_authorization.adapter +3 -0
  11. data/lib/hecks/adapters/driven/governance_authorization.rb +101 -0
  12. data/lib/hecks/adapters/driven/heki/journal.rb +56 -0
  13. data/lib/hecks/adapters/driven/heki/saga_store.rb +99 -0
  14. data/lib/hecks/adapters/driven/heki/snapshot.rb +65 -0
  15. data/lib/hecks/adapters/driven/heki.adapter +4 -0
  16. data/lib/hecks/adapters/driven/heki.rb +179 -0
  17. data/lib/hecks/adapters/driven/identity_registry.adapter +3 -0
  18. data/lib/hecks/adapters/driven/identity_registry.rb +26 -0
  19. data/lib/hecks/adapters/driven/in_memory_ordering.rb +51 -0
  20. data/lib/hecks/adapters/driven/lambda/client.rb +63 -0
  21. data/lib/hecks/adapters/driven/lambda.adapter +4 -0
  22. data/lib/hecks/adapters/driven/lambda.rb +145 -0
  23. data/lib/hecks/adapters/driven/memory.adapter +3 -0
  24. data/lib/hecks/adapters/driven/memory.rb +103 -0
  25. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +3 -0
  26. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +28 -0
  27. data/lib/hecks/adapters/driven/postgres/codec.rb +88 -0
  28. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +207 -0
  29. data/lib/hecks/adapters/driven/postgres.adapter +5 -0
  30. data/lib/hecks/adapters/driven/postgres.rb +438 -0
  31. data/lib/hecks/adapters/driven/postgres_era.adapter +17 -0
  32. data/lib/hecks/adapters/driven/prism.adapter +3 -0
  33. data/lib/hecks/adapters/driven/prism.rb +80 -0
  34. data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
  35. data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
  36. data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
  37. data/lib/hecks/adapters/driven/sqlite/codec.rb +80 -0
  38. data/lib/hecks/adapters/driven/sqlite/projection.rb +172 -0
  39. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +192 -0
  40. data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
  41. data/lib/hecks/adapters/driven/sqlite.rb +306 -0
  42. data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
  43. data/lib/hecks/adapters/driven/system_clock.rb +14 -0
  44. data/lib/hecks/adapters/driven.rb +54 -0
  45. data/lib/hecks/adapters.rb +6 -0
  46. data/lib/hecks/behaviors/dsl.rb +125 -0
  47. data/lib/hecks/behaviors/expectations.rb +340 -0
  48. data/lib/hecks/behaviors/ir.rb +31 -0
  49. data/lib/hecks/behaviors/rspec.rb +42 -0
  50. data/lib/hecks/behaviors/runner.rb +96 -0
  51. data/lib/hecks/behaviors.rb +25 -0
  52. data/lib/hecks/bluebook/aggregate.rb +108 -0
  53. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
  54. data/lib/hecks/bluebook/assembly/build.rb +48 -0
  55. data/lib/hecks/bluebook/assembly/contract.rb +112 -0
  56. data/lib/hecks/bluebook/assembly/contracts.rb +438 -0
  57. data/lib/hecks/bluebook/assembly/marks.rb +228 -0
  58. data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
  59. data/lib/hecks/bluebook/assembly.rb +91 -0
  60. data/lib/hecks/bluebook/attribute.rb +96 -0
  61. data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
  62. data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
  63. data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
  64. data/lib/hecks/bluebook/behaviour/command.rb +116 -0
  65. data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
  66. data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
  67. data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
  68. data/lib/hecks/bluebook/behaviour/lifecycle.rb +68 -0
  69. data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
  70. data/lib/hecks/bluebook/behaviour/process_manager.rb +51 -0
  71. data/lib/hecks/bluebook/behaviour/query.rb +10 -0
  72. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
  73. data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
  74. data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
  75. data/lib/hecks/bluebook/chapter.rb +78 -0
  76. data/lib/hecks/bluebook/command.rb +124 -0
  77. data/lib/hecks/bluebook/domain_port.rb +102 -0
  78. data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
  79. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +1018 -0
  80. data/lib/hecks/bluebook/dsl/attribute_collector.rb +348 -0
  81. data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
  82. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1087 -0
  83. data/lib/hecks/bluebook/dsl/command_builder.rb +714 -0
  84. data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
  85. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
  86. data/lib/hecks/bluebook/dsl/entity_builder.rb +430 -0
  87. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
  88. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
  89. data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
  90. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
  91. data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
  92. data/lib/hecks/bluebook/dsl/policy_builder.rb +135 -0
  93. data/lib/hecks/bluebook/dsl/port_builder.rb +39 -0
  94. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
  95. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +306 -0
  96. data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
  97. data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
  98. data/lib/hecks/bluebook/dsl/rule_reference.rb +173 -0
  99. data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
  100. data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
  101. data/lib/hecks/bluebook/dsl/word_gate.rb +228 -0
  102. data/lib/hecks/bluebook/dsl/world_builder.rb +117 -0
  103. data/lib/hecks/bluebook/dsl.rb +45 -0
  104. data/lib/hecks/bluebook/entity.rb +103 -0
  105. data/lib/hecks/bluebook/expression/canonical_form.rb +123 -0
  106. data/lib/hecks/bluebook/expression/evaluator.rb +304 -0
  107. data/lib/hecks/bluebook/expression/projection.json +218 -0
  108. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
  109. data/lib/hecks/bluebook/expression/resolver.rb +781 -0
  110. data/lib/hecks/bluebook/expression.rb +13 -0
  111. data/lib/hecks/bluebook/hexagon.rb +60 -0
  112. data/lib/hecks/bluebook/lifecycle.rb +42 -0
  113. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
  114. data/lib/hecks/bluebook/meta_validator/judge.rb +621 -0
  115. data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
  116. data/lib/hecks/bluebook/meta_validator/port_judge.rb +51 -0
  117. data/lib/hecks/bluebook/meta_validator/readings.rb +360 -0
  118. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +351 -0
  119. data/lib/hecks/bluebook/meta_validator/shapes.rb +266 -0
  120. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
  121. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
  122. data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
  123. data/lib/hecks/bluebook/meta_validator.rb +542 -0
  124. data/lib/hecks/bluebook/model_check.rb +445 -0
  125. data/lib/hecks/bluebook/pattern_subset.rb +184 -0
  126. data/lib/hecks/bluebook/policy.rb +41 -0
  127. data/lib/hecks/bluebook/process_manager.rb +128 -0
  128. data/lib/hecks/bluebook/project_discovery.rb +30 -0
  129. data/lib/hecks/bluebook/project_loader.rb +40 -0
  130. data/lib/hecks/bluebook/project_register.rb +82 -0
  131. data/lib/hecks/bluebook/query.rb +61 -0
  132. data/lib/hecks/bluebook/read_model.rb +109 -0
  133. data/lib/hecks/bluebook/reference.rb +74 -0
  134. data/lib/hecks/bluebook/smoke_test.rb +166 -0
  135. data/lib/hecks/bluebook/synthesizer.rb +95 -0
  136. data/lib/hecks/bluebook/translation.rb +92 -0
  137. data/lib/hecks/bluebook/value_object.rb +66 -0
  138. data/lib/hecks/bluebook.rb +74 -0
  139. data/lib/hecks/codemod.rb +342 -0
  140. data/lib/hecks/construct.rb +71 -0
  141. data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
  142. data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
  143. data/lib/hecks/deploy/oidc.json +18 -0
  144. data/lib/hecks/doc/reference.rb +410 -0
  145. data/lib/hecks/embryonaut_bluebook.rb +75 -0
  146. data/lib/hecks/facade/cli_door.rb +119 -0
  147. data/lib/hecks/facade/cli_runner.rb +190 -0
  148. data/lib/hecks/facade/command_request.rb +105 -0
  149. data/lib/hecks/facade/handle.rb +173 -0
  150. data/lib/hecks/facade/json_door.rb +166 -0
  151. data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
  152. data/lib/hecks/facade/surface/chapter.rb +107 -0
  153. data/lib/hecks/facade/surface.rb +48 -0
  154. data/lib/hecks/facade.rb +44 -0
  155. data/lib/hecks/forms/app.rb +341 -0
  156. data/lib/hecks/forms/command_form_renderer.rb +113 -0
  157. data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
  158. data/lib/hecks/forms/field_renderer.rb +177 -0
  159. data/lib/hecks/forms/field_shape.rb +232 -0
  160. data/lib/hecks/forms/html.rb +84 -0
  161. data/lib/hecks/forms/index_renderer.rb +35 -0
  162. data/lib/hecks/forms/page.rb +157 -0
  163. data/lib/hecks/forms/params.rb +161 -0
  164. data/lib/hecks/forms/port_argument.rb +46 -0
  165. data/lib/hecks/forms/query_form_renderer.rb +114 -0
  166. data/lib/hecks/forms/record_renderer.rb +119 -0
  167. data/lib/hecks/forms/record_table.rb +68 -0
  168. data/lib/hecks/forms/reference_options.rb +30 -0
  169. data/lib/hecks/forms/value_object_shape.rb +46 -0
  170. data/lib/hecks/forms.rb +54 -0
  171. data/lib/hecks/fqn.rb +94 -0
  172. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
  173. data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
  174. data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
  175. data/lib/hecks/framework/bluebook/governance.bluebook +145 -0
  176. data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
  177. data/lib/hecks/framework/oidc.json +39 -0
  178. data/lib/hecks/framework.rb +90 -0
  179. data/lib/hecks/freezer.rb +67 -0
  180. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +527 -0
  181. data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
  182. data/lib/hecks/fuzzing/isolated_boot.rb +286 -0
  183. data/lib/hecks/fuzzing/properties.rb +1192 -0
  184. data/lib/hecks/fuzzing/replay.rb +668 -0
  185. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
  186. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +80 -0
  187. data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
  188. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +250 -0
  189. data/lib/hecks/fuzzing/sequence_generator.rb +131 -0
  190. data/lib/hecks/fuzzing/value_generator.rb +200 -0
  191. data/lib/hecks/fuzzing.rb +12 -0
  192. data/lib/hecks/grammar/evolve.rb +324 -0
  193. data/lib/hecks/grammar/expression.bluebook +420 -0
  194. data/lib/hecks/grammar/expression_operators.json +1648 -0
  195. data/lib/hecks/grammar/grammar.hecksagon +20 -0
  196. data/lib/hecks/grammar/oidc.json +69 -0
  197. data/lib/hecks/grammar/translation.bluebook +203 -0
  198. data/lib/hecks/grammar.rb +178 -0
  199. data/lib/hecks/ir.rb +126 -0
  200. data/lib/hecks/language/adapter.bluebook +116 -0
  201. data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
  202. data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
  203. data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
  204. data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
  205. data/lib/hecks/language/bluebook/command.bluebook +471 -0
  206. data/lib/hecks/language/bluebook/entity.bluebook +392 -0
  207. data/lib/hecks/language/bluebook/policy.bluebook +189 -0
  208. data/lib/hecks/language/bluebook/process_manager.bluebook +381 -0
  209. data/lib/hecks/language/bluebook/projection.bluebook +267 -0
  210. data/lib/hecks/language/bluebook/query.bluebook +245 -0
  211. data/lib/hecks/language/bluebook/shape.bluebook +292 -0
  212. data/lib/hecks/language/bluebook/syntax.bluebook +448 -0
  213. data/lib/hecks/language/bluebook/vocabulary.bluebook +379 -0
  214. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
  215. data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
  216. data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
  217. data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
  218. data/lib/hecks/language/oidc.json +333 -0
  219. data/lib/hecks/language/port.bluebook +120 -0
  220. data/lib/hecks/language/translation/translation.bluebook +110 -0
  221. data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
  222. data/lib/hecks/language/world/wiring.bluebook +62 -0
  223. data/lib/hecks/language/world/world.bluebook +84 -0
  224. data/lib/hecks/literal.rb +125 -0
  225. data/lib/hecks/naming.rb +174 -0
  226. data/lib/hecks/ports/access_control.port +9 -0
  227. data/lib/hecks/ports/access_control.rb +62 -0
  228. data/lib/hecks/ports/agent/answers.rb +104 -0
  229. data/lib/hecks/ports/agent.port +8 -0
  230. data/lib/hecks/ports/agent.rb +167 -0
  231. data/lib/hecks/ports/authentication.port +6 -0
  232. data/lib/hecks/ports/authentication.rb +50 -0
  233. data/lib/hecks/ports/authorization.port +7 -0
  234. data/lib/hecks/ports/authorization.rb +62 -0
  235. data/lib/hecks/ports/clock.port +5 -0
  236. data/lib/hecks/ports/clock.rb +62 -0
  237. data/lib/hecks/ports/extraction.port +5 -0
  238. data/lib/hecks/ports/extraction.rb +37 -0
  239. data/lib/hecks/ports/identity_assignment.port +5 -0
  240. data/lib/hecks/ports/identity_assignment.rb +45 -0
  241. data/lib/hecks/ports/identity_generation.port +5 -0
  242. data/lib/hecks/ports/identity_generation.rb +49 -0
  243. data/lib/hecks/ports/identity_resolution.port +5 -0
  244. data/lib/hecks/ports/identity_resolution.rb +40 -0
  245. data/lib/hecks/ports/loading.port +4 -0
  246. data/lib/hecks/ports/loading.rb +13 -0
  247. data/lib/hecks/ports/persistence/append_only.rb +138 -0
  248. data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
  249. data/lib/hecks/ports/persistence/execution.rb +23 -0
  250. data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
  251. data/lib/hecks/ports/persistence/plugin.rb +54 -0
  252. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +169 -0
  253. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +128 -0
  254. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +161 -0
  255. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +61 -0
  256. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +304 -0
  257. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +171 -0
  258. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +190 -0
  259. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +478 -0
  260. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +166 -0
  261. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +314 -0
  262. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +168 -0
  263. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +170 -0
  264. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +134 -0
  265. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +137 -0
  266. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +89 -0
  267. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +85 -0
  268. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
  269. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +132 -0
  270. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +74 -0
  271. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +758 -0
  272. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +120 -0
  273. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +31 -0
  274. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +46 -0
  275. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +102 -0
  276. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +46 -0
  277. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +70 -0
  278. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +72 -0
  279. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +120 -0
  280. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +183 -0
  281. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +41 -0
  282. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +28 -0
  283. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +30 -0
  284. data/lib/hecks/ports/persistence/plugins/era/translation.rb +11 -0
  285. data/lib/hecks/ports/persistence/plugins/era.rb +48 -0
  286. data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
  287. data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
  288. data/lib/hecks/ports/persistence.port +4 -0
  289. data/lib/hecks/ports/persistence.rb +39 -0
  290. data/lib/hecks/ports/projection.port +4 -0
  291. data/lib/hecks/ports/projection.rb +96 -0
  292. data/lib/hecks/ports/query/in_memory.rb +60 -0
  293. data/lib/hecks/ports/query/ordering.rb +41 -0
  294. data/lib/hecks/ports/query.rb +36 -0
  295. data/lib/hecks/ports.rb +27 -0
  296. data/lib/hecks/projections/diagrams.rb +642 -0
  297. data/lib/hecks/projections/ir.rb +18 -0
  298. data/lib/hecks/projections/model/deviations.rb +98 -0
  299. data/lib/hecks/projections/model.rb +145 -0
  300. data/lib/hecks/projections/oidc.rb +110 -0
  301. data/lib/hecks/projections/parser_table.rb +159 -0
  302. data/lib/hecks/projections/reference.rb +38 -0
  303. data/lib/hecks/projections/shape.rb +44 -0
  304. data/lib/hecks/projections/statements.rb +110 -0
  305. data/lib/hecks/projections/vocabulary.rb +100 -0
  306. data/lib/hecks/projections.rb +32 -0
  307. data/lib/hecks/projector/cli_projector.rb +446 -0
  308. data/lib/hecks/projector/docs_projector.rb +321 -0
  309. data/lib/hecks/projector/exporter.rb +158 -0
  310. data/lib/hecks/projector/ir_projector.rb +18 -0
  311. data/lib/hecks/projector/narrate_projector.rb +243 -0
  312. data/lib/hecks/projector/target.rb +97 -0
  313. data/lib/hecks/projector.rb +186 -0
  314. data/lib/hecks/query_ir.rb +411 -0
  315. data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
  316. data/lib/hecks/query_specification/common/comparators.rb +25 -0
  317. data/lib/hecks/query_specification/common/comparison.rb +174 -0
  318. data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
  319. data/lib/hecks/query_specification/common/dsl.rb +58 -0
  320. data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
  321. data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
  322. data/lib/hecks/query_specification/common/null_policy.rb +92 -0
  323. data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
  324. data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
  325. data/lib/hecks/query_specification/common/options.rb +32 -0
  326. data/lib/hecks/query_specification/common/order_by.rb +9 -0
  327. data/lib/hecks/query_specification/common/specification.rb +10 -0
  328. data/lib/hecks/query_specification/common/where_clause.rb +9 -0
  329. data/lib/hecks/query_specification/field_path.rb +107 -0
  330. data/lib/hecks/query_specification/hop_path.rb +132 -0
  331. data/lib/hecks/query_specification/read_model/specification.rb +18 -0
  332. data/lib/hecks/query_specification.rb +14 -0
  333. data/lib/hecks/rendering.rb +48 -0
  334. data/lib/hecks/router/namespace_installer.rb +157 -0
  335. data/lib/hecks/router.rb +70 -0
  336. data/lib/hecks/runtime/aggregate_lock.rb +45 -0
  337. data/lib/hecks/runtime/boot_gates.rb +41 -0
  338. data/lib/hecks/runtime/caller.rb +66 -0
  339. data/lib/hecks/runtime/capability_graph.rb +44 -0
  340. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +135 -0
  341. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +260 -0
  342. data/lib/hecks/runtime/command_interpreter.rb +497 -0
  343. data/lib/hecks/runtime/command_rules/admissibility.rb +357 -0
  344. data/lib/hecks/runtime/command_rules/arithmetic.rb +260 -0
  345. data/lib/hecks/runtime/command_rules/authorization.rb +65 -0
  346. data/lib/hecks/runtime/command_rules/emission.rb +34 -0
  347. data/lib/hecks/runtime/command_rules/references.rb +189 -0
  348. data/lib/hecks/runtime/command_rules.rb +28 -0
  349. data/lib/hecks/runtime/dependency_planning.rb +245 -0
  350. data/lib/hecks/runtime/dispatcher.rb +301 -0
  351. data/lib/hecks/runtime/entity_element.rb +253 -0
  352. data/lib/hecks/runtime/entity_interpreter.rb +299 -0
  353. data/lib/hecks/runtime/errors.rb +122 -0
  354. data/lib/hecks/runtime/event.rb +51 -0
  355. data/lib/hecks/runtime/identity.rb +156 -0
  356. data/lib/hecks/runtime/instance.rb +175 -0
  357. data/lib/hecks/runtime/interpreting.rb +90 -0
  358. data/lib/hecks/runtime/loader.rb +184 -0
  359. data/lib/hecks/runtime/policy_interpreter.rb +366 -0
  360. data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
  361. data/lib/hecks/runtime/query_interpreter.rb +286 -0
  362. data/lib/hecks/runtime/reaction_invocation.rb +253 -0
  363. data/lib/hecks/runtime/read_model_interpreter.rb +341 -0
  364. data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
  365. data/lib/hecks/runtime/reference_hop.rb +99 -0
  366. data/lib/hecks/runtime/refusal_wording.rb +126 -0
  367. data/lib/hecks/runtime/registry/saga_persistence.rb +142 -0
  368. data/lib/hecks/runtime/registry/verification.rb +246 -0
  369. data/lib/hecks/runtime/registry.rb +279 -0
  370. data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
  371. data/lib/hecks/runtime/routing.rb +96 -0
  372. data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
  373. data/lib/hecks/runtime/saga_interpreter.rb +503 -0
  374. data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -0
  375. data/lib/hecks/runtime/tenant_check.rb +84 -0
  376. data/lib/hecks/runtime/tenant_scope.rb +55 -0
  377. data/lib/hecks/runtime/value/admission.rb +130 -0
  378. data/lib/hecks/runtime/value/coercion.rb +565 -0
  379. data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
  380. data/lib/hecks/runtime/value.rb +125 -0
  381. data/lib/hecks/runtime.rb +107 -0
  382. data/lib/hecks/storehouse.rb +632 -0
  383. data/lib/hecks/version.rb +16 -0
  384. data/lib/hecks/vocabulary.rb +218 -0
  385. data/lib/hecks.rb +123 -6
  386. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
  387. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
  388. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
  389. metadata +412 -222
  390. data/bin/hecks +0 -7
  391. data/bin/hecks-package +0 -65
  392. data/bin/hecks_console +0 -12
  393. data/bin/hecks_serverless +0 -6
  394. data/lib/cli/build.rb +0 -14
  395. data/lib/cli/command_runner.rb +0 -28
  396. data/lib/cli/console.rb +0 -10
  397. data/lib/cli/generate.rb +0 -37
  398. data/lib/cli/hecks-cli.rb +0 -27
  399. data/lib/cli/test.rb +0 -57
  400. data/lib/console/commands.rb +0 -8
  401. data/lib/console/hecks-console.rb +0 -1
  402. data/lib/packager/README.md +0 -0
  403. data/lib/packager/app_runner.rb +0 -21
  404. data/lib/packager/args.rb +0 -26
  405. data/lib/packager/compatibility/fixnum.rb +0 -6
  406. data/lib/packager/hecks.rb +0 -39
  407. data/lib/packager/query_runner.rb +0 -21
  408. data/lib/packager/resources/Dockerfile +0 -11
  409. data/lib/packager/resources/app_binary +0 -7
  410. data/lib/packager/resources/bundle_config +0 -3
  411. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
  412. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
  413. data/lib/serverless/Domain +0 -32
  414. data/lib/serverless/cli.rb +0 -75
  415. data/lib/serverless/resources/command_name.js +0 -5
  416. data/lib/serverless/resources/environment.js +0 -7
  417. data/lib/serverless/resources/handler.js.tt +0 -28
  418. data/lib/serverless/resources/run_binary.js +0 -22
  419. data/lib/serverless/resources/serverless.yml +0 -20
@@ -0,0 +1,93 @@
1
+ module Hecks
2
+ module Fuzzing
3
+ class SequenceGenerator
4
+ # Everything the booted domain offers, indexed for the picker:
5
+ # creating vs instance commands, entity commands, queries, and the
6
+ # populators that predict where an appended element lands.
7
+ module Catalog
8
+ private
9
+
10
+ def build_catalog(runtime)
11
+ creating, instance, entity_commands, queries, entity_queries = [], [], [], [], []
12
+ read_models = []
13
+
14
+ runtime.registry.bluebooks.each do |domain_name, bluebook|
15
+ bluebook.aggregates.each do |aggregate|
16
+ aggregate.commands.each do |command|
17
+ entry = { verb: "#{domain_name}::#{aggregate.hecks_name}.#{command.hecks_name}",
18
+ command: command, aggregate: aggregate }
19
+ (command.creates? ? creating : instance) << entry
20
+ end
21
+ aggregate.queries.each do |query|
22
+ queries << { verb: "#{domain_name}::#{aggregate.hecks_name}.#{query.name}",
23
+ query: query, aggregate: aggregate }
24
+ end
25
+ aggregate.entities.each do |entity|
26
+ entity.commands.each do |command|
27
+ entity_commands << { verb: "#{domain_name}::#{aggregate.hecks_name}.#{entity.hecks_name}.#{command.hecks_name}",
28
+ command: command, aggregate: aggregate, entity: entity }
29
+ end
30
+ entity.queries.each do |query|
31
+ entity_queries << { verb: "#{domain_name}::#{aggregate.hecks_name}.#{entity.hecks_name}.#{query.name}",
32
+ query: query, aggregate: aggregate, entity: entity }
33
+ end
34
+ end
35
+ end
36
+
37
+ # THE BARE DOMAIN FORM — "Domain.report_name", no "::" — the
38
+ # SAME shape `Dispatcher#query` itself branches on to route a
39
+ # read-model ask apart from an aggregate query. A report was
40
+ # never in this catalog at all before: `aggregation_matches_
41
+ # recompute` (count/median) has only ever been exercised by
42
+ # hand-built specs, never a single real generated sequence,
43
+ # because nothing here ever asked one. `model:`, not
44
+ # `aggregate:` — a rootless report has no aggregate of its
45
+ # own to be eligible against (see picker.rb's own use of
46
+ # `model.reference_target`).
47
+ bluebook.read_models.each do |model|
48
+ read_models << { verb: "#{domain_name}.#{model.query_name}", model: model }
49
+ end
50
+ end
51
+
52
+ { creating: creating, instance: instance, entity_commands: entity_commands,
53
+ queries: queries, entity_queries: entity_queries, read_models: read_models,
54
+ populators: populators(runtime),
55
+ # Which aggregates this corpus can actually make one of — the ones
56
+ # `satisfiable?` is entitled to wait for.
57
+ creatable: creating.map { |entry| entry[:aggregate].hecks_name }.to_set }
58
+ end
59
+
60
+ # Which command, on which aggregate, appends to which entity list — so a
61
+ # successful dispatch can predict the identity the element it just added
62
+ # landed on. Entity#identified_by is filled by `Array(current).size + 1`
63
+ # (CommandInterpreter#entity_element) when the append's own field
64
+ # mapping doesn't already assign it — the common case, predicted here.
65
+ # A domain whose append explicitly assigns identity through a mapped
66
+ # argument is covered too, without guessing: whatever value THIS
67
+ # generator supplied for that argument at dispatch time IS the
68
+ # identity, and gets recorded directly (see `record_outcome`).
69
+ def populators(runtime)
70
+ runtime.registry.bluebooks.each_value.flat_map do |bluebook|
71
+ bluebook.aggregates.flat_map do |aggregate|
72
+ aggregate.commands.filter_map do |command|
73
+ append = command.mutations.find { |mutation| mutation.op == :append }
74
+ next unless append
75
+
76
+ list_attribute = aggregate.attribute(append.target)
77
+ next unless list_attribute&.list?
78
+
79
+ entity = aggregate.entities.find { |candidate| candidate.hecks_name == list_attribute.type.to_s }
80
+ next unless entity
81
+
82
+ identity_field = entity.identified_by
83
+ mapped = identity_field && append.source[identity_field]
84
+ { command: command, aggregate: aggregate, entity: entity,
85
+ identity_field: identity_field, identity_argument: mapped.is_a?(Symbol) ? mapped : nil }
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,80 @@
1
+ require_relative "../value_generator"
2
+ require_relative "../../naming"
3
+
4
+ module Hecks
5
+ module Fuzzing
6
+ class SequenceGenerator
7
+ # What a successful step created, and how a later step draws on it —
8
+ # the state tracking that makes multi-step sequences reach further
9
+ # than single calls ever did.
10
+ module OutcomeTracker
11
+ private
12
+
13
+ def record_outcome(catalog, entry, args)
14
+ aggregate = entry[:aggregate]
15
+ parent_scalar = identity_scalar_of(aggregate, args)
16
+
17
+ @known_ids[aggregate.hecks_name] << parent_scalar if entry[:entity].nil? && entry[:command].creates?
18
+
19
+ populator = catalog[:populators].find { |p| p[:command].equal?(entry[:command]) && p[:aggregate].equal?(aggregate) }
20
+ return unless populator
21
+
22
+ key = "#{aggregate.hecks_name}.#{populator[:entity].hecks_name}##{parent_scalar}"
23
+
24
+ # The auto-minted case (CommandInterpreter#entity_element): the
25
+ # element just landed at count-so-far + 1. The explicit case: this
26
+ # generator itself supplied whatever value sits at
27
+ # args[identity_argument] — no guessing needed, it's read straight
28
+ # back.
29
+ new_id =
30
+ if populator[:identity_argument]
31
+ ValueGenerator.scalar_of(args[populator[:identity_argument].to_s])
32
+ else
33
+ (@entity_known_ids[key].size + 1).to_s
34
+ end
35
+ @entity_known_ids[key] << new_id
36
+ end
37
+
38
+ # THE SCALAR THIS STEP'S OWN AGGREGATE IDENTITY RESOLVES TO, from the
39
+ # args a creating (or entity-populating) command actually
40
+ # dispatched — a SINGLE declared head (or the untyped default
41
+ # `:id`) reads straight off its own top-level arg the way this
42
+ # always has (`ValueGenerator.scalar_of`, opening the identity value
43
+ # object). A genuinely COMPOSITE identity (`composite_identity?`,
44
+ # shared with StepBuilder#add_identity! — see its own comment) has
45
+ # no such top-level arg at all: `add_identity!` deliberately leaves
46
+ # a composite creating command's own individually-declared fields
47
+ # alone rather than forcing a synthetic `id` neither the bluebook
48
+ # nor the command ever declared, so this joins those fields itself,
49
+ # in the SAME declaration order `Runtime::Identity.of` joins them
50
+ # at dispatch (`Naming.identity`, `Naming::IDENTITY_JOIN`) — which
51
+ # is exactly what makes the result usable later as the bare `id:`
52
+ # a composite Revoke/act-again command expects (`Identity.from`'s
53
+ # own untyped fallback, reached when `identity_of` finds no
54
+ # top-level field args to join, reads a bare `id:` straight
55
+ # through unattributed).
56
+ def identity_scalar_of(aggregate, args)
57
+ parent_key = (aggregate.identified_by || :id).to_s
58
+ return ValueGenerator.scalar_of(args[parent_key]) unless composite_identity?(aggregate)
59
+
60
+ parts = aggregate.identity_paths.map { |path| ValueGenerator.scalar_of(args[path.to_s.split(".").first]) }
61
+ Naming.identity(parts)
62
+ end
63
+
64
+ def pick_known(name)
65
+ pool = @known_ids[name]
66
+ return ValueGenerator.random_id(@random) if pool.empty? || @random.rand < ValueGenerator::INVALID_REFERENCE_PROBABILITY
67
+
68
+ pool.sample(random: @random)
69
+ end
70
+
71
+ def pick_entity_known(aggregate_name, entity_name, parent_id)
72
+ pool = @entity_known_ids["#{aggregate_name}.#{entity_name}##{parent_id}"]
73
+ return ValueGenerator.random_id(@random) if pool.empty? || @random.rand < ValueGenerator::INVALID_REFERENCE_PROBABILITY
74
+
75
+ pool.sample(random: @random)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,96 @@
1
+ module Hecks
2
+ module Fuzzing
3
+ class SequenceGenerator
4
+ # Which step to try next: eligibility (what is POSSIBLE from the
5
+ # state so far) and weighting (what is LIKELY to reach somewhere
6
+ # new).
7
+ module Picker
8
+ private
9
+
10
+ def pick(catalog)
11
+ rest = catalog[:queries].dup
12
+ catalog[:instance].each { |entry| rest << entry if actionable?(catalog, entry) }
13
+ catalog[:entity_commands].each { |entry| rest << entry if actionable?(catalog, entry) }
14
+ catalog[:entity_queries].each { |entry| rest << entry if @known_ids[entry[:aggregate].hecks_name].any? }
15
+ catalog[:read_models].each { |entry| rest << entry if read_model_actionable?(entry) }
16
+
17
+ makers = catalog[:creating].select { |entry| satisfiable?(catalog, entry) }
18
+ pool = rest + makers.flat_map { |entry| [entry] * creating_weight(rest.size) }
19
+
20
+ steer(pool).sample(random: @random)
21
+ end
22
+
23
+ # WHILE THERE IS NOTHING TO FIND, MAKING SOMETHING IS THE ONLY USEFUL MOVE.
24
+ #
25
+ # A flat weight is right once the domain has records in it, and badly
26
+ # wrong before: banking declares ten queries, and from an empty store
27
+ # exactly ONE creating command is satisfiable — Customer.Register, the
28
+ # verb the whole cascade waits on. Two entries against ten left runs
29
+ # spending their entire budget querying a store nothing had been written
30
+ # to ; one seed answered ten empty queries in a row and emitted nothing
31
+ # at all.
32
+ #
33
+ # So while nothing exists, creation matches everything else put together.
34
+ # After that the ordinary weight applies and the run gets on with
35
+ # exercising what it made.
36
+ def creating_weight(rest_size)
37
+ return CREATING_WEIGHT if @known_ids.each_value.any?(&:any?)
38
+
39
+ [rest_size, CREATING_WEIGHT].max
40
+ end
41
+
42
+ def actionable?(catalog, entry)
43
+ @known_ids[entry[:aggregate].hecks_name].any? && satisfiable?(catalog, entry)
44
+ end
45
+
46
+ # A ROOTLESS report (no `reference_to` at all) has nothing to wait
47
+ # for — it reads whole tables, the same "always eligible" position
48
+ # `catalog[:queries]` itself takes. A ROOTED one needs a real
49
+ # instance of its own `reference_target` to ask about first, same
50
+ # rule `actionable?` already gives an instance command — asking
51
+ # `Banking.disputed_payment_count` before any Account exists would
52
+ # just be refused (`refuse_object_reference`/`NotFound`) the same
53
+ # way `Account.Credit` would be, for the identical reason.
54
+ def read_model_actionable?(entry)
55
+ entry[:model].reference_target.nil? || @known_ids[entry[:model].reference_target].any?
56
+ end
57
+
58
+ # A COMMAND THAT REFERENCES NOTHING THAT EXISTS CANNOT SUCCEED, so it is
59
+ # not offered until something does.
60
+ #
61
+ # `ValueGenerator.reference_value` has no real id to hand over when its
62
+ # pool is empty, so it mints `missing-…` and the dispatch is refused
63
+ # before it starts — a guaranteed waste of a step, and worse, one that
64
+ # never fills the pool it was waiting on. Banking cascades from it:
65
+ # Account.Open needs a Customer, ATMCard.Issue needs an Account,
66
+ # Transfer.Request needs two, so a run that opened with the wrong verb
67
+ # spent its whole budget being refused.
68
+ #
69
+ # This is the rule instance commands already follow — `known_ids.any?` —
70
+ # applied to what a command REFERENCES rather than to what it acts on.
71
+ # A target no creating command in this corpus can make (a cross-domain
72
+ # reference, which `CommandRules#resolve_references` skips anyway) is
73
+ # exempt, or the whole domain would starve waiting for it.
74
+ def satisfiable?(catalog, entry)
75
+ entry[:command].attributes.select(&:reference?).all? do |attribute|
76
+ target = attribute.type.target_name.to_s
77
+ !catalog[:creatable].include?(target) || @known_ids[target].any?
78
+ end
79
+ end
80
+
81
+ # Coverage-guided rather than uniformly random : a verb this sequence has
82
+ # not dispatched yet is weighted up, so a run spends its budget on the
83
+ # commands it has not reached instead of re-rolling the ones it has. The
84
+ # eligibility rules above still decide what is POSSIBLE — this only decides
85
+ # what is likely, so a verb gated behind state it does not have yet stays
86
+ # out of the pool entirely rather than being preferred forever.
87
+ def steer(pool)
88
+ fresh = pool.reject { |entry| @exercised.include?(entry[:verb]) }
89
+ return pool if fresh.empty?
90
+
91
+ pool + (fresh * UNEXERCISED_WEIGHT)
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,250 @@
1
+ require_relative "../../bluebook/expression/resolver"
2
+ require_relative "../invalid_value_generator"
3
+ require_relative "../value_generator"
4
+ require_relative "../../runtime/errors"
5
+ require_relative "../../runtime/value"
6
+ require_relative "../../naming"
7
+
8
+ module Hecks
9
+ module Fuzzing
10
+ class SequenceGenerator
11
+ # Turn a picked entry into a corpus step: generate its arguments,
12
+ # occasionally malform exactly one of them, shape its identity, and
13
+ # dispatch it for real.
14
+ module StepBuilder
15
+ private
16
+
17
+ def build_query_step(runtime, entry)
18
+ args = args_for(entry[:query].attributes, entry[:aggregate])
19
+ safe_call { runtime.query(entry[:verb], **symbolize(args)) }
20
+ { "query" => entry[:verb], "args" => args }
21
+ end
22
+
23
+ # A REPORT ASK — the bare domain form `entry[:verb]` already carries
24
+ # ("Domain.report_name", no "::"), so `Dispatcher#query` routes it
25
+ # to the read model rather than an aggregate query. A `ReadModel`
26
+ # has no declared `.attributes` the way a `Query` does — its own
27
+ # argument surface is exactly ONE key, `reference_name`, and ONLY
28
+ # for a rooted model (`read_model_actionable?` already gated a
29
+ # rootless one straight into eligibility with nothing to supply).
30
+ # A BARE scalar, not `identity_shaped` — `ReadModelInterpreter#
31
+ # refuse_object_reference` explicitly REJECTS a Hash/Value offered
32
+ # here (this is the one place in the whole generator where the
33
+ # subject's own identity must NOT be wrapped the way a command
34
+ # argument's would be).
35
+ def build_read_model_step(runtime, entry)
36
+ model = entry[:model]
37
+ args = model.reference_target.nil? ? {} : { model.reference_name.to_s => pick_known(model.reference_target) }
38
+
39
+ safe_call { runtime.query(entry[:verb], **symbolize(args)) }
40
+ { "query" => entry[:verb], "args" => args }
41
+ end
42
+
43
+ def build_command_step(runtime, catalog, entry)
44
+ args = args_for(entry[:command].attributes, entry[:aggregate])
45
+ add_identity!(args, entry)
46
+
47
+ outcome = safe_call { runtime.dispatch(entry[:verb], **symbolize(args)) }
48
+ if outcome
49
+ record_outcome(catalog, entry, args)
50
+ @event_count += outcome.events.length
51
+ end
52
+ { "verb" => entry[:verb], "args" => args }
53
+ end
54
+
55
+ def args_for(attributes, aggregate)
56
+ args = attributes.each_with_object({}) do |attribute, built|
57
+ # AN OPTIONAL ARGUMENT IS SOMETIMES NOT GIVEN, and that is an
58
+ # ordinary payload rather than a damaged one — see
59
+ # OPTIONAL_OMITTED_PROBABILITY for why this cannot live in
60
+ # `malform` below and what it was costing while it did not
61
+ # exist at all.
62
+ next if attribute.optional? && @random.rand < SequenceGenerator::OPTIONAL_OMITTED_PROBABILITY
63
+
64
+ if attribute.list?
65
+ value = list_value_for(attribute, aggregate)
66
+ # A list-of-ENTITY command attribute has no real example
67
+ # anywhere in this repo's domains — every entity-owned list is
68
+ # populated via a per-element append command instead, never a
69
+ # whole-list command argument — so `list_value_for` (below)
70
+ # answers `nil` for one rather than guessing at an entity's own
71
+ # shape, and this step still skips it exactly as it always
72
+ # has. A list-of-VALUE-OBJECT attribute (`ConsoleSettings::
73
+ # Collection.ReplaceColumns`' own `columns`, `list_of(Column)`)
74
+ # is the real, previously-unfuzzable case this now covers —
75
+ # `sets :columns` imports the owner aggregate's own declared
76
+ # `list_of` attribute onto the command verbatim (Command
77
+ # Builder#resolve_bare_set!), so it is a required, ordinary
78
+ # top-level argument like any other, not an entity mutation.
79
+ next if value.nil?
80
+
81
+ built[attribute.name.to_s] = value
82
+ else
83
+ built[attribute.name.to_s] = ValueGenerator.value_for(attribute, aggregate, random: @random, known_ids: @known_ids)
84
+ end
85
+ end
86
+
87
+ malform(args, attributes, aggregate)
88
+ end
89
+
90
+ # A `list_of` ATTRIBUTE'S OWN VALUE — an array of independently
91
+ # generated elements, each shaped exactly the way a bare (non-list)
92
+ # attribute of the SAME declared element type already is
93
+ # (`ValueGenerator.value_for`), since `list_of(X)`'s own element
94
+ # coercion is `X`'s ordinary shape repeated, not a different one
95
+ # (`Attribute#type` is already unwrapped from `list_of(...)` at
96
+ # declare time — the same fact `command_builder.rb`'s own comment
97
+ # on `resolve_bare_set!` names). 0-3 elements — enough to exercise a
98
+ # genuinely non-trivial replace without generating pathologically
99
+ # large payloads every time. `nil`, not `[]`, when the element type
100
+ # isn't a declared value object at all (an entity-typed list) — see
101
+ # `args_for`'s own comment on why that case still skips rather than
102
+ # guesses.
103
+ def list_value_for(attribute, aggregate)
104
+ value_object = Runtime::Value.value_object_for(aggregate, attribute.type.to_s)
105
+ return nil unless value_object
106
+
107
+ Array.new(@random.rand(0..3)) { ValueGenerator.value_for(attribute, aggregate, random: @random, known_ids: @known_ids) }
108
+ end
109
+
110
+ # ONE MALFORMATION AT A TIME, and usually none. A step whose payload is
111
+ # wrong in three ways only ever proves which check runs first ; wrong in
112
+ # exactly one way names the check that fired. And the rate stays low on
113
+ # purpose — a corrupted step is almost always refused, a sequence of
114
+ # refusals reaches no state at all, and bin/fuzz already counts those as
115
+ # SILENT rather than scoring them.
116
+ def malform(args, attributes, aggregate)
117
+ return args if args.empty? || @random.rand >= MALFORMED_ARGUMENT_PROBABILITY
118
+
119
+ case @random.rand(3)
120
+ when 0 then corrupt_one(args, attributes, aggregate)
121
+ when 1 then drop_one(args, aggregate)
122
+ else args.merge([InvalidValueGenerator.undeclared_argument(random: @random)].to_h)
123
+ end
124
+ end
125
+
126
+ # NEVER THE IDENTITY. A creating command with no id auto-mints one, and
127
+ # a minted id is deliberately unreproducible — a random hex, never a
128
+ # guessable counter — so dropping it manufactures a step whose outcome
129
+ # cannot be replayed and says nothing about the runtime's behaviour.
130
+ # Every step in the hand-written corpus
131
+ # supplies an id for the same reason. Whether an auto-minted id OUGHT to
132
+ # be reproducible is a real question, but it is not one a payload fuzzer
133
+ # can ask.
134
+ def drop_one(args, aggregate)
135
+ identity = (aggregate.identified_by || :id).to_s
136
+ droppable = args.keys - [identity, "id"]
137
+ return args if droppable.empty?
138
+
139
+ args.reject { |name, _| name == droppable.sample(random: @random) }
140
+ end
141
+
142
+ def corrupt_one(args, attributes, aggregate)
143
+ named = attributes.reject(&:list?).select { |attribute| args.key?(attribute.name.to_s) }
144
+ return args if named.empty?
145
+
146
+ attribute = named.sample(random: @random)
147
+ args.merge(attribute.name.to_s => InvalidValueGenerator.corrupt(attribute, aggregate, random: @random))
148
+ end
149
+
150
+ def add_identity!(args, entry)
151
+ aggregate = entry[:aggregate]
152
+ parent_key = (aggregate.identified_by || :id).to_s
153
+
154
+ if entry[:entity]
155
+ parent_scalar = pick_known(aggregate.hecks_name)
156
+ args[parent_key] = identity_shaped(aggregate, aggregate.identified_by, parent_scalar, aggregate)
157
+ entity_key = (entry[:entity].identified_by || :id).to_s
158
+ entity_scalar = pick_entity_known(aggregate.hecks_name, entry[:entity].hecks_name, parent_scalar)
159
+ args[entity_key] = identity_shaped(entry[:entity], entry[:entity].identified_by, entity_scalar, aggregate)
160
+ elsif entry[:command].creates?
161
+ # A COMPOSITE IDENTITY (`identified_by` answering nil with MORE
162
+ # THAN ONE declared path — Behaviour::Identified's own "a
163
+ # composite has no single head" comment) supplies every one of
164
+ # its parts as its own ordinary, individually-declared command
165
+ # attribute already — `RoleAssignment::Assign` takes actor_id/
166
+ # role_name/starts_at directly, `args_for` (above) already
167
+ # generated all three. Forcing a synthetic top-level `id` here
168
+ # too — this codebase's own fallback for the SINGLE-key and the
169
+ # genuinely untyped (`identity_paths.empty?`, no `identified_by`
170
+ # declared at all) cases — hands a composite creating command
171
+ # an argument it never declared at all, refused every time as
172
+ # unknown before this check existed (a creating `Assign`/`Grant`
173
+ # step was never anything BUT refused). `identity_paths.empty?`
174
+ # is the untyped default (falls all the way back to a minted
175
+ # `:id` the runtime itself never declared as an attribute
176
+ # either), which still needs exactly the old minting behavior.
177
+ unless composite_identity?(aggregate)
178
+ args[parent_key] ||= identity_shaped(aggregate, aggregate.identified_by, ValueGenerator.random_id(@random), aggregate)
179
+ end
180
+ else
181
+ scalar = pick_known(aggregate.hecks_name)
182
+ args[parent_key] = identity_shaped(aggregate, aggregate.identified_by, scalar, aggregate)
183
+ end
184
+ end
185
+
186
+ # TRUE ONLY FOR A GENUINE MULTI-FIELD IDENTITY — `identified_by`
187
+ # returns nil both for a real composite (`identity_paths.size > 1`)
188
+ # and for the untyped default with NO identity declared at all
189
+ # (`identity_paths.size == 0`, Behaviour::Identified's own
190
+ # `Array(@identified_by)` fallback) ; only the first of those two
191
+ # has its own parts already sitting in `args` as real, individually-
192
+ # generated command attributes.
193
+ def composite_identity?(aggregate) = aggregate.identified_by.nil? && aggregate.identity_paths.size > 1
194
+
195
+ # A bare scalar id, shaped to match whatever `construct` itself
196
+ # declares that identity field as. `Account::LedgerEntry` is addressed
197
+ # by `sequence`, and `sequence` is declared as a value-object-typed
198
+ # attribute (`LedgerSequence`), not a plain identifier — a fuzz run
199
+ # that skipped this wrapping and dispatched a bare `"1"` had every
200
+ # such step refused at the type gate (a bare scalar against a
201
+ # value-object-typed identity is a TypeMismatch, not a NotFound), so
202
+ # the wrapping is what lets a fuzz step address the record at
203
+ # all. Left as a bare scalar when the construct declares no such
204
+ # attribute at all — the default `:id` case, which really is untyped.
205
+ def identity_shaped(construct, key, scalar, aggregate)
206
+ return scalar unless key
207
+
208
+ attribute = construct.attribute(key)
209
+ return scalar unless attribute
210
+
211
+ value_object = aggregate.value_object(attribute.type.to_s)
212
+ return scalar unless value_object
213
+
214
+ field = value_object.attributes.first
215
+ return scalar unless field
216
+
217
+ { field.name.to_s => coerce_scalar(field.type.to_s, scalar) }
218
+ end
219
+
220
+ def coerce_scalar(type_name, scalar)
221
+ case type_name
222
+ when "Integer" then scalar.to_i
223
+ when "Float" then scalar.to_f
224
+ else scalar.to_s
225
+ end
226
+ end
227
+
228
+ def symbolize(args) = args.transform_keys(&:to_sym)
229
+
230
+ # A step the runtime declines is not a generator failure — it simply did
231
+ # not take effect, so nothing is recorded and the sequence carries on. The
232
+ # step still goes into the corpus, because a REFUSAL IS AN ANSWER, and
233
+ # its wording is pinned by the corpus.
234
+ #
235
+ # EvaluationError sits alongside the declared refusals deliberately : a
236
+ # payload the interpreter cannot read is the domain declining it, and
237
+ # bin/run already records exactly those in a corpus's refusals —
238
+ # `positive? expects a number, got "lots"` is one of banking's. Anything
239
+ # else still propagates and fails spec/fuzzing, which is what says the
240
+ # generator built a step that breaks the interpreter for reasons that have
241
+ # nothing to do with the domain declining a payload.
242
+ def safe_call
243
+ yield
244
+ rescue *Hecks::Runtime::DOMAIN_REFUSALS, Hecks::Bluebook::Expression::EvaluationError
245
+ nil
246
+ end
247
+ end
248
+ end
249
+ end
250
+ end
@@ -0,0 +1,131 @@
1
+ require "fileutils"
2
+ require "set"
3
+ require "tmpdir"
4
+ require_relative "isolated_boot"
5
+ require_relative "invalid_value_generator"
6
+ require_relative "value_generator"
7
+ require_relative "sequence_generator/catalog"
8
+ require_relative "sequence_generator/picker"
9
+ require_relative "sequence_generator/step_builder"
10
+ require_relative "sequence_generator/outcome_tracker"
11
+
12
+ module Hecks
13
+ module Fuzzing
14
+ # A random-but-valid sequence of dispatches and queries, in the exact
15
+ # `{name, note, steps}` shape `spec/corpus/*.json` already uses — so a
16
+ # generated sequence replays as a corpus member, completely unchanged.
17
+ # Boots a throwaway copy of
18
+ # the domain and DISPATCHES each candidate step for real as it builds the
19
+ # sequence (not just synthesizing plausible-looking JSON) : the only way to
20
+ # know whether a step actually reached a new state, or which id an
21
+ # auto-minted entity landed on, is to run it and watch what happened.
22
+ #
23
+ # Single-call fuzzing mostly misses the bugs this project has actually
24
+ # found — they needed STATE first (a saga leg acting on a transfer that
25
+ # already exists, a reference pointing at a customer already registered).
26
+ # So this tracks what it has created as it goes, the same way
27
+ # spec/banking_state_machine_spec.rb's hand-written generator does, and
28
+ # weights later steps toward acting on what already exists.
29
+ #
30
+ # One concern per file beside this one: what the domain offers
31
+ # (sequence_generator/catalog.rb), which step to try next (picker.rb),
32
+ # how a step is built and dispatched (step_builder.rb), and what a
33
+ # success taught us (outcome_tracker.rb).
34
+ class SequenceGenerator
35
+ include Catalog
36
+ include Picker
37
+ include StepBuilder
38
+ include OutcomeTracker
39
+
40
+ # Creating commands are always eligible ; weighting them heavier (not
41
+ # exclusively — a domain with only one or two aggregates would starve
42
+ # everything else) makes a sequence reach an actionable state sooner
43
+ # instead of spending its early budget refusing "acts on nothing yet."
44
+ CREATING_WEIGHT = 2
45
+
46
+ # How often a payload is deliberately the wrong shape. Low, because a
47
+ # refused step reaches no new state and a sequence of them is SILENT.
48
+ MALFORMED_ARGUMENT_PROBABILITY = 0.12
49
+
50
+ # HOW OFTEN AN OPTIONAL ARGUMENT IS SIMPLY NOT GIVEN — a fair coin,
51
+ # because that is exactly what `optional:` means: present or absent,
52
+ # both legal, neither the interesting case.
53
+ #
54
+ # This is NOT a malformation and must not be filed as one. `malform`
55
+ # drops an argument too, but a dropped REQUIRED argument is refused,
56
+ # a refusal writes no record, and bin/fuzz counts it SILENT — so the
57
+ # one outcome worth reaching (a stored record carrying a null, then
58
+ # QUERIED) was unreachable from that path by construction. Until
59
+ # this existed no generated history contained a null at all, which
60
+ # meant `query_answers_match_reference` — the differential that
61
+ # diffs every native adapter against the reference interpreter — had
62
+ # never once compared a nullable field. Four query bugs shipped in
63
+ # exactly that blind spot (`ne:` with an empty string, array `in:`,
64
+ # `ne:` against a null, `in` on a numeric field); the adapter
65
+ # agreement gate had the identical hole and found a fourth bug the
66
+ # hour it was closed.
67
+ OPTIONAL_OMITTED_PROBABILITY = 0.5
68
+
69
+ # How strongly an unexercised verb is preferred over one this sequence has
70
+ # already dispatched. Random picking revisits the same handful of verbs and
71
+ # leaves whole commands untouched for a whole run — which is the same
72
+ # "reached no interesting state" problem the SILENT count reports, seen from
73
+ # the generating end rather than the scoring end.
74
+ UNEXERCISED_WEIGHT = 4
75
+
76
+ def self.generate(domain_path, seed:, steps:, adapter: :memory)
77
+ new(domain_path, seed: seed, steps: steps, adapter: adapter).call
78
+ end
79
+
80
+ # How many EVENTS the generated sequence actually produced — not
81
+ # steps, not successful dispatches, but the sum of every Result#events
82
+ # length across the run. This is the count bin/fuzz declares as the
83
+ # script's own `expectations.events` claim: whatever was achieved
84
+ # DURING generation becomes the claim a fresh replay of the same
85
+ # script is held to. Zero means the sequence never
86
+ # reached an interesting state — a fuzzer-effectiveness fact, not a
87
+ # replay one.
88
+ attr_reader :event_count
89
+
90
+ def initialize(domain_path, seed:, steps:, adapter: :memory)
91
+ @domain_path = domain_path
92
+ @seed = seed
93
+ @step_count = steps
94
+ @adapter = adapter
95
+ @random = Random.new(seed)
96
+ @known_ids = Hash.new { |h, k| h[k] = [] }
97
+ @entity_known_ids = Hash.new { |h, k| h[k] = [] }
98
+ @exercised = Set.new
99
+ @event_count = 0
100
+ end
101
+
102
+ def call
103
+ # Real leftover data from ordinary use (bin/console, whatever) lives
104
+ # under the example's data/ — a generator that boots against it
105
+ # starts from state its own known_ids tracking doesn't know about.
106
+ # IsolatedBoot resets that AND rebinds persistence to Memory, since
107
+ # a Postgres-bound domain's real store lives outside the copied
108
+ # directory entirely and `rm_rf`ing data/ alone cannot reach it —
109
+ # see isolated_boot.rb's own header.
110
+ IsolatedBoot.call(@domain_path, adapter: @adapter) do |copy|
111
+ runtime = Hecks.boot(copy)
112
+ catalog = build_catalog(runtime)
113
+ Array.new(@step_count) { attempt_step(runtime, catalog) }.compact
114
+ end
115
+ end
116
+
117
+ private
118
+
119
+ def attempt_step(runtime, catalog)
120
+ entry = pick(catalog)
121
+ return nil unless entry
122
+
123
+ @exercised << entry[:verb]
124
+ if entry[:query] then build_query_step(runtime, entry)
125
+ elsif entry[:model] then build_read_model_step(runtime, entry)
126
+ else build_command_step(runtime, catalog, entry)
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end