hecks 1.3.0 → 1.5.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 (373) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +72 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +83 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
  7. data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
  9. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  10. data/lib/hecks/adapters/driven/heki.rb +106 -9
  11. data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
  12. data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
  13. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  15. data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
  16. data/lib/hecks/adapters/driven/lambda.rb +82 -35
  17. data/lib/hecks/adapters/driven/local_storage.rb +83 -10
  18. data/lib/hecks/adapters/driven/memory.rb +205 -9
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -1
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  24. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  25. data/lib/hecks/adapters/driven/prism.rb +50 -11
  26. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  31. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  32. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  33. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  34. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  35. data/lib/hecks/adapters/driven.rb +6 -4
  36. data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
  37. data/lib/hecks/behaviors/dsl.rb +60 -2
  38. data/lib/hecks/behaviors/expectations.rb +190 -29
  39. data/lib/hecks/behaviors/ir.rb +12 -1
  40. data/lib/hecks/behaviors/rspec.rb +9 -1
  41. data/lib/hecks/behaviors/runner.rb +21 -2
  42. data/lib/hecks/behaviors.rb +9 -1
  43. data/lib/hecks/bluebook/aggregate.rb +43 -13
  44. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
  45. data/lib/hecks/bluebook/assembly/build.rb +26 -5
  46. data/lib/hecks/bluebook/assembly/contract.rb +98 -23
  47. data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
  48. data/lib/hecks/bluebook/assembly/marks.rb +159 -30
  49. data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
  50. data/lib/hecks/bluebook/assembly.rb +32 -14
  51. data/lib/hecks/bluebook/attribute.rb +26 -12
  52. data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
  53. data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
  54. data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
  55. data/lib/hecks/bluebook/behaviour/command.rb +55 -25
  56. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
  57. data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
  58. data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
  59. data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
  60. data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
  61. data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
  62. data/lib/hecks/bluebook/behaviour/query.rb +6 -1
  63. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
  64. data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
  65. data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
  66. data/lib/hecks/bluebook/capabilities.rb +27 -0
  67. data/lib/hecks/bluebook/chapter.rb +51 -9
  68. data/lib/hecks/bluebook/command.rb +62 -17
  69. data/lib/hecks/bluebook/domain_port.rb +34 -9
  70. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  71. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  72. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +282 -123
  73. data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
  74. data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
  75. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
  76. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
  77. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  78. data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
  79. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  80. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  81. data/lib/hecks/bluebook/dsl/entity_builder.rb +191 -61
  82. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  83. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
  84. data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
  85. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  86. data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
  87. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  88. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  89. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +111 -47
  90. data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
  91. data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
  92. data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
  93. data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
  94. data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
  95. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  96. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  97. data/lib/hecks/bluebook/entity.rb +40 -11
  98. data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
  99. data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
  100. data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
  101. data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
  102. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
  103. data/lib/hecks/bluebook/expression/resolver.rb +369 -128
  104. data/lib/hecks/bluebook/hexagon.rb +35 -1
  105. data/lib/hecks/bluebook/lifecycle.rb +12 -1
  106. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
  107. data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
  108. data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
  109. data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
  110. data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
  111. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
  112. data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
  113. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
  114. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
  115. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
  116. data/lib/hecks/bluebook/meta_validator.rb +235 -139
  117. data/lib/hecks/bluebook/model_check.rb +434 -104
  118. data/lib/hecks/bluebook/pattern_subset.rb +32 -10
  119. data/lib/hecks/bluebook/policy.rb +15 -13
  120. data/lib/hecks/bluebook/process_manager.rb +27 -14
  121. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  122. data/lib/hecks/bluebook/project_loader.rb +40 -0
  123. data/lib/hecks/bluebook/project_register.rb +50 -6
  124. data/lib/hecks/bluebook/query.rb +31 -4
  125. data/lib/hecks/bluebook/read_model.rb +35 -15
  126. data/lib/hecks/bluebook/reference.rb +26 -13
  127. data/lib/hecks/bluebook/smoke_test.rb +46 -23
  128. data/lib/hecks/bluebook/synthesizer.rb +46 -12
  129. data/lib/hecks/bluebook/translation.rb +34 -5
  130. data/lib/hecks/bluebook/value_object.rb +29 -11
  131. data/lib/hecks/bluebook.rb +5 -6
  132. data/lib/hecks/codemod.rb +138 -50
  133. data/lib/hecks/construct.rb +21 -7
  134. data/lib/hecks/corpus.rb +438 -0
  135. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  136. data/lib/hecks/doc/reference.rb +200 -31
  137. data/lib/hecks/embryonaut_bluebook.rb +38 -15
  138. data/lib/hecks/facade/cli_door.rb +69 -10
  139. data/lib/hecks/facade/cli_runner.rb +105 -24
  140. data/lib/hecks/facade/command_request.rb +23 -0
  141. data/lib/hecks/facade/handle.rb +155 -35
  142. data/lib/hecks/facade/json_door.rb +106 -25
  143. data/lib/hecks/facade/surface/aggregate_door.rb +50 -27
  144. data/lib/hecks/facade/surface/chapter.rb +26 -17
  145. data/lib/hecks/facade/surface.rb +16 -3
  146. data/lib/hecks/facade.rb +15 -4
  147. data/lib/hecks/forms/app.rb +46 -30
  148. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  149. data/lib/hecks/forms/field_renderer.rb +142 -6
  150. data/lib/hecks/forms/field_shape.rb +183 -19
  151. data/lib/hecks/forms/html.rb +51 -7
  152. data/lib/hecks/forms/index_renderer.rb +14 -2
  153. data/lib/hecks/forms/page.rb +14 -0
  154. data/lib/hecks/forms/params.rb +120 -23
  155. data/lib/hecks/forms/port_argument.rb +14 -2
  156. data/lib/hecks/forms/query_form_renderer.rb +65 -2
  157. data/lib/hecks/forms/record_renderer.rb +60 -2
  158. data/lib/hecks/forms/record_table.rb +28 -1
  159. data/lib/hecks/forms/reference_options.rb +24 -0
  160. data/lib/hecks/forms/value_object_shape.rb +13 -3
  161. data/lib/hecks/forms.rb +24 -4
  162. data/lib/hecks/fqn.rb +59 -1
  163. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  164. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  165. data/lib/hecks/framework/oidc.json +15 -0
  166. data/lib/hecks/framework.rb +79 -25
  167. data/lib/hecks/freezer.rb +27 -11
  168. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
  169. data/lib/hecks/fuzzing/combination_miner.rb +178 -0
  170. data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
  171. data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
  172. data/lib/hecks/fuzzing/differential.rb +192 -0
  173. data/lib/hecks/fuzzing/domain_generator.rb +871 -0
  174. data/lib/hecks/fuzzing/era_boundary.rb +68 -19
  175. data/lib/hecks/fuzzing/form_census.rb +121 -20
  176. data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
  177. data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
  178. data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
  179. data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
  180. data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
  181. data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
  182. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
  183. data/lib/hecks/fuzzing/properties/guards.rb +86 -42
  184. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
  185. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
  186. data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
  187. data/lib/hecks/fuzzing/properties/querying.rb +82 -28
  188. data/lib/hecks/fuzzing/properties.rb +84 -51
  189. data/lib/hecks/fuzzing/qa_settings.rb +164 -0
  190. data/lib/hecks/fuzzing/replay.rb +241 -133
  191. data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
  192. data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
  193. data/lib/hecks/fuzzing/self_consistency.rb +315 -137
  194. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
  195. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
  196. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
  197. data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
  198. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
  199. data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
  200. data/lib/hecks/fuzzing/shrinker.rb +309 -0
  201. data/lib/hecks/fuzzing/structural_skips.rb +37 -130
  202. data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
  203. data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
  204. data/lib/hecks/fuzzing/value_generator.rb +110 -22
  205. data/lib/hecks/fuzzing.rb +5 -0
  206. data/lib/hecks/grammar/evolve.rb +188 -12
  207. data/lib/hecks/grammar.rb +53 -7
  208. data/lib/hecks/ir.rb +51 -20
  209. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  210. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  211. data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
  212. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  213. data/lib/hecks/language/oidc.json +5 -0
  214. data/lib/hecks/literal.rb +41 -9
  215. data/lib/hecks/naming.rb +112 -31
  216. data/lib/hecks/ports/access_control.rb +53 -2
  217. data/lib/hecks/ports/agent/answers.rb +83 -6
  218. data/lib/hecks/ports/agent.rb +119 -35
  219. data/lib/hecks/ports/authentication.rb +44 -4
  220. data/lib/hecks/ports/authorization.rb +50 -11
  221. data/lib/hecks/ports/clock.rb +42 -23
  222. data/lib/hecks/ports/extraction.rb +16 -0
  223. data/lib/hecks/ports/identity_assignment.rb +23 -2
  224. data/lib/hecks/ports/identity_generation.rb +17 -3
  225. data/lib/hecks/ports/identity_resolution.rb +17 -1
  226. data/lib/hecks/ports/key_vault.port +6 -0
  227. data/lib/hecks/ports/key_vault.rb +58 -0
  228. data/lib/hecks/ports/loading.rb +4 -0
  229. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  230. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  231. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  232. data/lib/hecks/ports/persistence/execution.rb +4 -0
  233. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  234. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  235. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  236. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  237. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  238. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  239. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +34 -5
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  250. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
  251. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  252. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +95 -6
  253. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  254. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
  255. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  256. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  257. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  258. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +48 -8
  259. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  260. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  261. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  262. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +58 -19
  263. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
  264. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  265. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -1
  266. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  267. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  268. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  269. data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
  270. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  271. data/lib/hecks/ports/persistence.rb +36 -1
  272. data/lib/hecks/ports/projection.rb +61 -7
  273. data/lib/hecks/ports/query/in_memory.rb +41 -8
  274. data/lib/hecks/ports/query/ordering.rb +21 -6
  275. data/lib/hecks/ports/query.rb +35 -0
  276. data/lib/hecks/ports.rb +1 -0
  277. data/lib/hecks/projections/bootstrap_table.rb +147 -0
  278. data/lib/hecks/projections/diagrams.rb +315 -79
  279. data/lib/hecks/projections/glossary/html.rb +91 -3
  280. data/lib/hecks/projections/glossary/markdown.rb +50 -3
  281. data/lib/hecks/projections/glossary/mermaid.rb +49 -1
  282. data/lib/hecks/projections/glossary/sections.rb +1 -1
  283. data/lib/hecks/projections/glossary/sentences.rb +110 -5
  284. data/lib/hecks/projections/glossary.rb +173 -31
  285. data/lib/hecks/projections/ir.rb +1 -1
  286. data/lib/hecks/projections/model/deviations.rb +62 -17
  287. data/lib/hecks/projections/model.rb +75 -21
  288. data/lib/hecks/projections/oidc.rb +45 -8
  289. data/lib/hecks/projections/parser_table.rb +53 -12
  290. data/lib/hecks/projections/reference.rb +15 -3
  291. data/lib/hecks/projections/rust_vocabulary.rb +646 -0
  292. data/lib/hecks/projections/shape.rb +10 -2
  293. data/lib/hecks/projections/statements.rb +69 -22
  294. data/lib/hecks/projections/vocabulary.rb +26 -9
  295. data/lib/hecks/projections.rb +5 -3
  296. data/lib/hecks/projector/cli_projector.rb +245 -37
  297. data/lib/hecks/projector/docs_projector.rb +154 -28
  298. data/lib/hecks/projector/exporter.rb +104 -29
  299. data/lib/hecks/projector/ir_projector.rb +7 -1
  300. data/lib/hecks/projector/narrate_projector.rb +144 -23
  301. data/lib/hecks/projector/target.rb +42 -18
  302. data/lib/hecks/projector.rb +86 -18
  303. data/lib/hecks/query_ir.rb +94 -47
  304. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  305. data/lib/hecks/query_specification/common/comparison.rb +109 -27
  306. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  307. data/lib/hecks/query_specification/common/null_policy.rb +61 -15
  308. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  309. data/lib/hecks/query_specification/common/options.rb +25 -0
  310. data/lib/hecks/query_specification/field_path.rb +69 -15
  311. data/lib/hecks/query_specification/hop_path.rb +57 -20
  312. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  313. data/lib/hecks/rendering.rb +9 -3
  314. data/lib/hecks/router/namespace_installer.rb +16 -3
  315. data/lib/hecks/router.rb +56 -1
  316. data/lib/hecks/runtime/aggregate_lock.rb +20 -11
  317. data/lib/hecks/runtime/boot_gates.rb +21 -3
  318. data/lib/hecks/runtime/caller.rb +40 -8
  319. data/lib/hecks/runtime/capability_graph.rb +13 -2
  320. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
  321. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
  322. data/lib/hecks/runtime/command_interpreter.rb +195 -113
  323. data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
  324. data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
  325. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  326. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  327. data/lib/hecks/runtime/command_rules/references.rb +145 -55
  328. data/lib/hecks/runtime/command_rules.rb +3 -0
  329. data/lib/hecks/runtime/dependency_planning.rb +56 -11
  330. data/lib/hecks/runtime/dispatcher.rb +263 -92
  331. data/lib/hecks/runtime/entity_element.rb +304 -67
  332. data/lib/hecks/runtime/entity_interpreter.rb +149 -88
  333. data/lib/hecks/runtime/errors.rb +37 -23
  334. data/lib/hecks/runtime/event.rb +15 -10
  335. data/lib/hecks/runtime/identity.rb +91 -23
  336. data/lib/hecks/runtime/instance.rb +103 -18
  337. data/lib/hecks/runtime/interpreting.rb +24 -16
  338. data/lib/hecks/runtime/invocation.rb +358 -0
  339. data/lib/hecks/runtime/loader.rb +105 -19
  340. data/lib/hecks/runtime/outbox.rb +164 -26
  341. data/lib/hecks/runtime/policy_interpreter.rb +73 -60
  342. data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
  343. data/lib/hecks/runtime/query_interpreter.rb +93 -74
  344. data/lib/hecks/runtime/reaction_invocation.rb +73 -28
  345. data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
  346. data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
  347. data/lib/hecks/runtime/reference_hop.rb +48 -6
  348. data/lib/hecks/runtime/refusal_wording.rb +142 -115
  349. data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
  350. data/lib/hecks/runtime/registry/verification.rb +153 -28
  351. data/lib/hecks/runtime/registry.rb +202 -32
  352. data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
  353. data/lib/hecks/runtime/routing.rb +36 -154
  354. data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
  355. data/lib/hecks/runtime/saga_interpreter.rb +90 -76
  356. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  357. data/lib/hecks/runtime/tenant_check.rb +33 -13
  358. data/lib/hecks/runtime/tenant_scope.rb +23 -5
  359. data/lib/hecks/runtime/value/admission.rb +75 -30
  360. data/lib/hecks/runtime/value/coercion.rb +555 -142
  361. data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
  362. data/lib/hecks/runtime/value.rb +71 -21
  363. data/lib/hecks/runtime.rb +39 -7
  364. data/lib/hecks/storehouse.rb +368 -72
  365. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  366. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  367. data/lib/hecks/version.rb +3 -3
  368. data/lib/hecks/vocabulary.rb +205 -4
  369. data/lib/hecks.rb +91 -11
  370. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
  371. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
  372. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
  373. metadata +28 -2
@@ -16,6 +16,7 @@ module Hecks
16
16
  # `validate_expect!` (private, below) is where a malformed or empty
17
17
  # `expect` is refused at build time rather than silently passing later.
18
18
  class TestCaseBuilder
19
+ # @param description [String] the `test "description" do ... end` text
19
20
  def initialize(description)
20
21
  @description = description
21
22
  @tests_command = nil
@@ -26,19 +27,46 @@ module Hecks
26
27
  @expect = {}
27
28
  end
28
29
 
30
+ # Records which command or query this test exercises.
31
+ #
32
+ # @param command [String, Symbol] the command or query verb, bare or dotted FQN
33
+ # @param on [String, Symbol, nil] the aggregate `command` is dispatched on, when
34
+ # `command` is not already a dotted FQN
35
+ # @param kind [Symbol] `:command` or `:query`
36
+ # @return [void]
29
37
  def tests(command, on: nil, kind: :command)
30
38
  @tests_command = command
31
39
  @on_aggregate = on
32
40
  @kind = kind
33
41
  end
34
42
 
43
+ # Records one setup command to dispatch before the test itself runs.
44
+ #
45
+ # @param command [String, Symbol] the setup command's verb
46
+ # @param kwargs [Hash{Symbol => Object}] the setup command's own arguments
47
+ # @return [void]
35
48
  def setup(command, **kwargs)
36
49
  @setups << TestSetup.new(command: command, args: kwargs)
37
50
  end
38
51
 
52
+ # Merges fields into the arguments the tested command or query is called with.
53
+ #
54
+ # @param kwargs [Hash{Symbol => Object}] argument names and values to merge in
55
+ # @return [Hash{Symbol => Object}] the input Hash so far
39
56
  def input(**kwargs) = @input.merge!(kwargs)
57
+
58
+ # Merges fields into what this test expects of the tested dispatch.
59
+ #
60
+ # @param kwargs [Hash{Symbol => Object}] expectation keys (`ok:`, `refused:`,
61
+ # `emits:`, `count:`, or a field name) and their expected values
62
+ # @return [Hash{Symbol => Object}] the expectation Hash so far
40
63
  def expect(**kwargs) = @expect.merge!(kwargs)
41
64
 
65
+ # Builds the `TestCase` this builder collected.
66
+ #
67
+ # @return [TestCase] the built test case
68
+ # @raise [Malformed] if `tests` was never called, `expect` is empty, or
69
+ # `expect` declares `count:` on a command or `emits:` on a query
42
70
  def build
43
71
  unless @tests_command
44
72
  raise Malformed, "test #{@description.inspect} never calls `tests` — " \
@@ -59,7 +87,7 @@ module Hecks
59
87
  # nothing and passes whenever dispatch doesn't raise; `count:` on a
60
88
  # command and `emits:` on a query are each read by neither runner
61
89
  # (Expectations#run_command/#run_query), so they're accepted here
62
- # and then silently ignored at run time. Checking both at BUILD
90
+ # and then silently ignored at run time. Checking both at build
63
91
  # time, not in the runners, makes them errors on the file that
64
92
  # wrote them rather than green checks nobody questions.
65
93
  def validate_expect!
@@ -84,6 +112,8 @@ module Hecks
84
112
  # `vision`/`loads`/`test` calls and builds a `BehaviorsSuite` (ir.rb),
85
113
  # refusing to build one missing either `vision` or `loads` (`#build`).
86
114
  class BehaviorsBuilder
115
+ # @param name [String] the suite's declared name
116
+ # @param source_path [String] the `.behaviors` file's own path
87
117
  def initialize(name, source_path:)
88
118
  @name = name
89
119
  @source_path = source_path
@@ -93,21 +123,40 @@ module Hecks
93
123
  @tests = []
94
124
  end
95
125
 
126
+ # Records the suite's one-line description of what it is examples of.
127
+ #
128
+ # @param text [String] the vision text
129
+ # @return [String] `text`, unchanged
96
130
  def vision(text) = @vision = text
97
131
 
98
- # Relative to THIS `.behaviors` file, never to the filesystem's cwd
132
+ # Relative to this `.behaviors` file, never to the filesystem's cwd
99
133
  # or a same-stem convention — scope is a fact this file declares,
100
134
  # not one a runner infers.
135
+ #
136
+ # @param paths [Array<String>] paths to the files this suite's domain boots
137
+ # from, relative to the `.behaviors` file
138
+ # @return [void]
101
139
  def loads(*paths)
102
140
  @loads = paths.map { |path| File.expand_path(path, @source_dir) }
103
141
  end
104
142
 
143
+ # Builds and records one test case.
144
+ #
145
+ # @param description [String] the test's own description
146
+ # @yield the test's body, evaluated against a `TestCaseBuilder`
147
+ # @return [void]
148
+ # @raise [Malformed] if the test never calls `tests`, has no `expect`, or
149
+ # declares `count:` on a command or `emits:` on a query
105
150
  def test(description, &block)
106
151
  builder = TestCaseBuilder.new(description)
107
152
  builder.instance_eval(&block) if block
108
153
  @tests << builder.build
109
154
  end
110
155
 
156
+ # Builds the `BehaviorsSuite` this builder collected.
157
+ #
158
+ # @return [BehaviorsSuite] the built suite
159
+ # @raise [Malformed] if the suite declares no `vision` or no `loads`
111
160
  def build
112
161
  unless @vision
113
162
  raise Malformed, "#{@source_path}: no `vision \"...\"` — say in one line " \
@@ -122,6 +171,15 @@ module Hecks
122
171
  tests: @tests, path: @source_path)
123
172
  end
124
173
 
174
+ # Builds a suite in one call: constructs a builder, evaluates `block`
175
+ # against it, and builds the result.
176
+ #
177
+ # @param name [String] the suite's declared name
178
+ # @param source_path [String] the `.behaviors` file's own path
179
+ # @yield the suite's body, evaluated against the new builder
180
+ # @return [BehaviorsSuite] the built suite
181
+ # @raise [Malformed] if the suite declares no `vision` or `loads`, or any of
182
+ # its `test` blocks is malformed
125
183
  def self.build(name, source_path:, &block)
126
184
  builder = new(name, source_path: source_path)
127
185
  builder.instance_eval(&block) if block
@@ -7,6 +7,8 @@ require_relative "../ports/persistence/binding_policy"
7
7
 
8
8
  # Hecks::Behaviors::Expectations
9
9
  #
10
+ # ## What it does
11
+ #
10
12
  # One test case, start to finish: take the suite's own runtime (booted
11
13
  # once for exactly what its `loads` names, reset to nothing between
12
14
  # tests — `runtime_for`), replay `setup` dispatches, dispatch (or query)
@@ -14,20 +16,22 @@ require_relative "../ports/persistence/binding_policy"
14
16
  # runner.rb the same way the file-count/sweep concern is split from a
15
17
  # single test's own execution.
16
18
  #
17
- # TWO DELIBERATE DEVIATIONS from a prior port of this same idea (read
19
+ # ## Deviations from a prior port
20
+ #
21
+ # Two deliberate deviations from a prior port of this same idea (read
18
22
  # before writing this, not reinvented):
19
23
  #
20
24
  # 1. A `setup` refusal and a refusal from the command under test are
21
- # caught in SEPARATE rescue scopes. The prior port wrapped the whole
25
+ # caught in separate rescue scopes. The prior port wrapped the whole
22
26
  # test body — setups included — in one `rescue *REFUSAL_CLASSES`, so a
23
27
  # broken setup could spuriously satisfy `expect refused: "..."` if its
24
28
  # own refusal happened to match the expected substring. Here, any
25
- # domain refusal during `setup` is unconditionally an ERROR — the
29
+ # domain refusal during `setup` is unconditionally an error — the
26
30
  # example never got to the situation it claims to test.
27
31
  #
28
- # 2. `emits:` is read off a `registry.event_log` DIFF around the tested
32
+ # 2. `emits:` is read off a `registry.event_log` diff around the tested
29
33
  # dispatch, not off `Result#events` alone. `Result#events` only holds
30
- # the events the OUTERMOST dispatch announced; a policy's own cascade
34
+ # the events the outermost dispatch announced; a policy's own cascade
31
35
  # reenters through the same `Dispatcher#dispatch` (`PolicyInterpreter
32
36
  # #deliver` → `door.reenter` → `dispatch`), and every dispatch's
33
37
  # events — outer and reentrant alike — land in the one shared
@@ -46,6 +50,15 @@ module Hecks
46
50
  REFUSAL_CLASSES = Hecks::Runtime::DOMAIN_REFUSALS
47
51
  SPECIAL_KEYS = %i[ok refused emits count].freeze
48
52
 
53
+ # Runs one test case: replays its `setup` dispatches, dispatches (or queries)
54
+ # the command under test, and checks its `expect`.
55
+ #
56
+ # @param test [Behaviors::TestCase] the test case to run
57
+ # @param suite [Behaviors::BehaviorsSuite] the suite `test` belongs to
58
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher, nil] an
59
+ # already-booted runtime to reuse and reset; nil boots (or reuses the cached
60
+ # boot of) `suite.loads` via `runtime_for`
61
+ # @return [Result] the test's pass, fail, or error outcome
49
62
  def run_one(test, suite, runtime: nil)
50
63
  runtime ||= runtime_for(suite)
51
64
  runtime.registry.reset_runtime_state!
@@ -66,18 +79,18 @@ module Hecks
66
79
  error_result(test, "#{e.class}: #{e.message}")
67
80
  end
68
81
 
69
- # ONE BOOT PER SUITE, NOT PER TEST. The isolation a test needs is a
82
+ # One boot per suite, not per test. The isolation a test needs is a
70
83
  # runtime with nothing in it — and a boot of the same files gives
71
84
  # exactly that back for the price of `Registry#reset_runtime_state!`
72
85
  # instead of ~2s of loading, verifying and era-checking the same
73
86
  # bluebook again (chess: 76 behaviours, 155s of which was booting
74
- # `chess.bluebook` 76 times). Keyed by the suite's own `loads` AND
87
+ # `chess.bluebook` 76 times). Keyed by the suite's own `loads` and
75
88
  # their mtimes, so an edited bluebook boots fresh on the next test
76
89
  # rather than running against a stale one — the property a watch
77
90
  # loop or a long rspec session actually relies on. `runtime:` lets
78
91
  # a caller that already holds a booted runtime (a spec, a REPL)
79
92
  # hand it in; it is reset the same way.
80
- # NOT frozen — a real cache, mutated below (`RUNTIMES[key] ||=
93
+ # Not frozen — a real cache, mutated below (`RUNTIMES[key] ||=
81
94
  # boot_and_guard(files)`) and by #reset!. False positive for
82
95
  # Style/MutableConstant.
83
96
  # rubocop:disable-next Style/MutableConstant
@@ -85,6 +98,14 @@ module Hecks
85
98
  RUNTIMES_LOCK = Mutex.new
86
99
  private_constant :RUNTIMES, :RUNTIMES_LOCK
87
100
 
101
+ # Boots (or reuses the cached boot of) the runtime a suite's `loads` names.
102
+ #
103
+ # @param suite [Behaviors::BehaviorsSuite] the suite whose `loads` files boot
104
+ # the runtime
105
+ # @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] the cached or
106
+ # freshly booted, Memory-only-guarded runtime
107
+ # @raise [Malformed] if any aggregate the suite boots is not bound to the
108
+ # default (Memory) adapter
88
109
  def runtime_for(suite)
89
110
  files = Array(suite.loads).map { |path| File.expand_path(path) }
90
111
  key = files.map { |file| [file, File.exist?(file) ? File.mtime(file).to_f : nil] }
@@ -94,22 +115,35 @@ module Hecks
94
115
  end
95
116
  end
96
117
 
97
- # `reset_runtime_state!` only drops repository OBJECTS between
118
+ # `reset_runtime_state!` only drops repository objects between
98
119
  # tests (registry.rb) — sufficient isolation for `Memory`, whose
99
120
  # `@records` is a plain per-instance ivar, so a fresh object really
100
121
  # is a fresh store. Against anything else (Sqlite, Postgres) the
101
122
  # rows themselves stay put: tests leak into each other, and a
102
- # suite booted against a domain's REAL hecksagon writes to a real
123
+ # suite booted against a domain's real hecksagon writes to a real
103
124
  # database. Refusing that wiring here, at boot, is the same shape
104
125
  # of guard `BindingPolicy` already applies to a missing bind — the
105
126
  # project's identity is refusing bad wiring up front, not
106
127
  # discovering it mid-suite.
128
+ #
129
+ # @param files [Array<String>] absolute paths to the bluebook/hecksagon/world
130
+ # files to boot
131
+ # @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted,
132
+ # Memory-only-guarded runtime
133
+ # @raise [Malformed] if any aggregate is bound to a non-Memory adapter
107
134
  def boot_and_guard(files)
108
135
  runtime = Hecks::Runtime::Loader.boot_files(files, install_facade: false)
109
136
  guard_memory_only!(runtime)
110
137
  runtime
111
138
  end
112
139
 
140
+ # Refuses a runtime where any aggregate is bound to anything other than the
141
+ # default (Memory) adapter, so tests can never leak state or touch a real store.
142
+ #
143
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted
144
+ # runtime to check
145
+ # @return [void]
146
+ # @raise [Malformed] if any aggregate is bound to a non-Memory adapter
113
147
  def guard_memory_only!(runtime)
114
148
  runtime.registry.bluebooks.each_value do |bluebook|
115
149
  bluebook.aggregates.each do |aggregate|
@@ -127,6 +161,15 @@ module Hecks
127
161
  end
128
162
  end
129
163
 
164
+ # Qualifies and dispatches (or queries) the command under test, catching a
165
+ # domain refusal as the test's own outcome rather than an error.
166
+ #
167
+ # @param test [Behaviors::TestCase] the test case to run
168
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the suite's
169
+ # booted runtime
170
+ # @param bluebooks [Array<Bluebook::Chapter>] every chapter the suite booted,
171
+ # searched to qualify a bare command/query name
172
+ # @return [Result] the test's pass, fail, or error outcome
130
173
  def run_tested(test, runtime, bluebooks)
131
174
  verb = qualify(test.tests_command, test.on_aggregate, bluebooks, kind: test.kind)
132
175
 
@@ -139,6 +182,13 @@ module Hecks
139
182
  check_refusal(test, e)
140
183
  end
141
184
 
185
+ # Dispatches the command under test and checks its `expect`.
186
+ #
187
+ # @param test [Behaviors::TestCase] the test case to run
188
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the suite's
189
+ # booted runtime
190
+ # @param verb [String] the command's dotted FQN
191
+ # @return [Result] the test's pass or fail outcome
142
192
  def run_command(test, runtime, verb)
143
193
  before = runtime.registry.event_log.length
144
194
  result = dispatch_command(runtime, verb, test.input)
@@ -159,17 +209,26 @@ module Hecks
159
209
  check_ok(test) || check_fields(test, settled_state(runtime, verb, result)) || pass_result(test)
160
210
  end
161
211
 
162
- # A field expectation reads the aggregate AS IT STANDS once the
212
+ # A field expectation reads the aggregate as it stands once the
163
213
  # dispatch and its whole cascade have run — the same "cascades are
164
214
  # always on" reading `emits:` already commits to. `Result#state` is
165
- # the wrong source for that: it snapshots the instance the OUTER
215
+ # the wrong source for that: it snapshots the instance the outer
166
216
  # dispatch saved, and a policy's own reentrant dispatch (a ply
167
217
  # advancing off a Moved event, a move count bumping) hydrates and
168
- # saves a FRESH record afterward — so a field the cascade wrote
218
+ # saves a fresh record afterward — so a field the cascade wrote
169
219
  # read back stale (found live: `expect move_count: 1` got 0 while
170
220
  # `emits:` saw MoveCountBumped in the same test). The repository
171
221
  # holds the settled record; read it back by the id the dispatch
172
222
  # itself answered with.
223
+ #
224
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the suite's
225
+ # booted runtime
226
+ # @param verb [String] the dispatched command's dotted FQN
227
+ # @param result [Runtime::Dispatcher::Result, Runtime::RemoteDispatcher::Result]
228
+ # the dispatch's own result
229
+ # @return [Hash{Symbol => Object}] the settled record's current state, read
230
+ # back from the repository; `result.state` (or `{}`) when the result has no
231
+ # id or the repository no longer has that aggregate/record
173
232
  def settled_state(runtime, verb, result)
174
233
  return result.state || {} unless result.respond_to?(:id) && result.id
175
234
 
@@ -185,7 +244,7 @@ module Hecks
185
244
  # A behaviors test writes a dispatch the way the guide's own chess
186
245
  # examples do — receiver identity and command facts side by side
187
246
  # (`label: "g", id: "wn", to: { file: 2, rank: 2 }`) — and since
188
- # #335 the dispatcher's own `to:` keyword is the ROUTING envelope,
247
+ # #335 the dispatcher's own `to:` keyword is the routing envelope,
189
248
  # so forwarding those kwargs loose collides the moment a domain
190
249
  # declares a command fact named `to` (chess does: every Move's own
191
250
  # destination). Found live: every such test failed with "to: does
@@ -202,15 +261,26 @@ module Hecks
202
261
  # expects a command's own declared attributes at the top level,
203
262
  # not a port operation's already-wrapped `to:`/`with:` shape.
204
263
  #
205
- # THIS USED TO RELY ON `resolve_target` RAISING `UnknownVerb` for
206
- # any port-operation verb — true only so long as nothing else ever
207
- # asked it to resolve one. Now that a `policy` can legitimately
208
- # `trigger` a port operation (`ReactionInvocation#resolve_target`'s
209
- # own port-operation branch), that raise is gone, so this checks
210
- # for a port operation directly instead of leaning on a refusal
211
- # that no longer happens.
264
+ # Checks for a port operation directly rather than leaning on
265
+ # `resolve_target` to raise `UnknownVerb` for one — a refusal that held
266
+ # only so long as nothing else ever asked it to resolve a port-operation
267
+ # verb. Now that a `policy` can legitimately `trigger` a port operation
268
+ # (`ReactionInvocation#resolve_target`'s own port-operation branch),
269
+ # that raise no longer happens, so this checks directly instead.
270
+ #
271
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the suite's
272
+ # booted runtime
273
+ # @param verb [String] the command's dotted FQN, or a port operation's
274
+ # @param args [Hash{Symbol => Object}] the command's (or setup's) facts, mixing
275
+ # receiver identity and declared arguments
276
+ # @return [Runtime::Dispatcher::Result, Runtime::RemoteDispatcher::Result] the
277
+ # dispatch's own result
278
+ # @raise [Runtime::UnknownVerb] if `verb` is not fully qualified or names an
279
+ # undeclared domain, aggregate, command, entity or port operation
280
+ # @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` when the
281
+ # domain refuses the call
212
282
  def dispatch_command(runtime, verb, args)
213
- return runtime.dispatch(verb, **args) if port_operation?(runtime, verb)
283
+ return runtime.dispatch_flat(verb, args) if port_operation?(runtime, verb)
214
284
 
215
285
  invocation = begin
216
286
  Runtime::ReactionInvocation.build(registry: runtime.registry, verb: verb,
@@ -218,7 +288,7 @@ module Hecks
218
288
  rescue Runtime::UnknownVerb
219
289
  nil
220
290
  end
221
- return runtime.dispatch(verb, **args) unless invocation
291
+ return runtime.dispatch_flat(verb, args) unless invocation
222
292
 
223
293
  if invocation.key?(:to)
224
294
  runtime.dispatch(verb, to: invocation[:to], with: invocation[:with])
@@ -227,10 +297,15 @@ module Hecks
227
297
  end
228
298
  end
229
299
 
230
- # THE SAME "Head.Rest" SHAPE `Dispatcher#dispatch` AND
231
- # `ReactionInvocation#resolve_target` BOTH ALREADY CHECK — a bare
300
+ # The same "Head.Rest" shape `Dispatcher#dispatch` and
301
+ # `ReactionInvocation#resolve_target` both already check — a bare
232
302
  # domain/aggregate lookup plus a port-name lookup, no command
233
303
  # resolution needed since all this asks is whether one exists.
304
+ #
305
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the suite's
306
+ # booted runtime
307
+ # @param verb [String] the verb to check, dotted FQN shaped
308
+ # @return [Boolean] true if `verb` names a port operation on a declared aggregate
234
309
  def port_operation?(runtime, verb)
235
310
  domain, aggregate_name, command_path = Naming.split_verb(verb)
236
311
  return false unless command_path
@@ -242,6 +317,13 @@ module Hecks
242
317
  rest && !!aggregate.port(head)
243
318
  end
244
319
 
320
+ # Runs the query under test and checks its `expect`.
321
+ #
322
+ # @param test [Behaviors::TestCase] the test case to run
323
+ # @param runtime [Runtime::Dispatcher, Runtime::RemoteDispatcher] the suite's
324
+ # booted runtime
325
+ # @param verb [String] the query's dotted FQN
326
+ # @return [Result] the test's pass or fail outcome
245
327
  def run_query(test, runtime, verb)
246
328
  rows = runtime.query(verb, **test.input)
247
329
 
@@ -267,6 +349,11 @@ module Hecks
267
349
  check_ok(test) || check_fields(test, row) || pass_result(test)
268
350
  end
269
351
 
352
+ # Whether `test.expect` names a field, not just one of the special keys.
353
+ #
354
+ # @param test [Behaviors::TestCase] the test case to check
355
+ # @return [Boolean] true if `test.expect` has any key besides `ok`, `refused`,
356
+ # `emits` or `count`
270
357
  def field_expectations?(test)
271
358
  test.expect.keys.any? { |key| !SPECIAL_KEYS.include?(key) }
272
359
  end
@@ -277,6 +364,11 @@ module Hecks
277
364
  # ambiguous about which row it's describing. `check_fields` itself
278
365
  # stays row-shaped (it already is, for `run_command`'s settled
279
366
  # state); this picks which row it reads.
367
+ #
368
+ # @param test [Behaviors::TestCase] the test case, for the fail message
369
+ # @param rows [Array<Hash>, Hash] the query's own result
370
+ # @return [Hash, Result] `rows` itself when it isn't an Array; its one row when
371
+ # it holds exactly one; otherwise a `fail_result` naming the row count
280
372
  def query_row(test, rows)
281
373
  return rows unless rows.is_a?(Array)
282
374
 
@@ -288,6 +380,11 @@ module Hecks
288
380
  end
289
381
  end
290
382
 
383
+ # Checks an `expect ok: true` expectation.
384
+ #
385
+ # @param test [Behaviors::TestCase] the test case to check
386
+ # @return [Result, nil] nil if `test.expect` has no `ok` key or expects `true`;
387
+ # a `fail_result` if it names anything else
291
388
  def check_ok(test)
292
389
  return unless test.expect.key?(:ok)
293
390
 
@@ -297,6 +394,13 @@ module Hecks
297
394
  fail_result(test, "expect ok: only accepts true — got #{expected.inspect}")
298
395
  end
299
396
 
397
+ # Checks every field-name expectation in `test.expect` against `state`.
398
+ #
399
+ # @param test [Behaviors::TestCase] the test case to check
400
+ # @param state [Hash{Symbol, String => Object}] the settled record's (or query
401
+ # row's) fields
402
+ # @return [Result, nil] nil if every expected field matches; a `fail_result` for
403
+ # the first field that names none of `state`'s keys or doesn't match
300
404
  def check_fields(test, state)
301
405
  test.expect.each do |key, expected|
302
406
  next if SPECIAL_KEYS.include?(key)
@@ -314,6 +418,12 @@ module Hecks
314
418
  nil
315
419
  end
316
420
 
421
+ # Checks a caught domain refusal against `test.expect[:refused]`.
422
+ #
423
+ # @param test [Behaviors::TestCase] the test case to check
424
+ # @param error [StandardError] the caught refusal, a member of `REFUSAL_CLASSES`
425
+ # @return [Result] a `pass_result` if `test` expected this refusal's message, an
426
+ # `error_result` if it expected none, or a `fail_result` if the message doesn't match
317
427
  def check_refusal(test, error)
318
428
  expected = test.expect[:refused]
319
429
  return error_result(test, "unexpected refusal (#{error.class}): #{error.message}") unless expected
@@ -331,8 +441,13 @@ module Hecks
331
441
  # `expect` values both ways — bare (`expect kind: "bishop"`) and
332
442
  # wrapped (`expect kind: { value: "bishop" }`). A live record's
333
443
  # field always comes back as a `Hecks::Runtime::Value`;
334
- # normalizing BOTH sides to the same bare-scalar-or-plain-hash
444
+ # normalizing both sides to the same bare-scalar-or-plain-hash
335
445
  # shape is the one comparison that accepts either spelling.
446
+ #
447
+ # @param value [Object] a stored field's value, or an `expect` value to compare
448
+ # it against
449
+ # @return [Object] the bare underlying value: unwrapped from a `Runtime::Value`,
450
+ # or from a `{value: ...}` Hash; unchanged otherwise
336
451
  def normalize(value)
337
452
  return Hecks::Runtime::Value.materialize_unwrapped(value) if value.is_a?(Hecks::Runtime::Value)
338
453
  return normalize(value[:value]) if value.is_a?(Hash) && value.keys == [:value]
@@ -344,9 +459,18 @@ module Hecks
344
459
  # can name more than one bluebook, so resolution searches every
345
460
  # aggregate across every bluebook the suite booted for the one that
346
461
  # actually declares the command. `on:` (when given, only ever on
347
- # the TESTED command — `setup` never receives it, see the DSL
462
+ # the tested command — `setup` never receives it, see the DSL
348
463
  # contract) narrows the search to one aggregate by name instead of
349
464
  # searching all of them.
465
+ #
466
+ # @param command [String, Symbol] a bare verb, or an already-dotted FQN
467
+ # @param on_aggregate [String, Symbol, nil] the aggregate to search, or nil to
468
+ # search every aggregate of every bluebook
469
+ # @param bluebooks [Array<Bluebook::Chapter>] every chapter the suite booted
470
+ # @param kind [Symbol] `:command` or `:query`
471
+ # @return [String] `command` unchanged if already dotted, otherwise its resolved
472
+ # dotted FQN
473
+ # @raise [ArgumentError] if no aggregate declares `command`, or more than one does
350
474
  def qualify(command, on_aggregate, bluebooks, kind:)
351
475
  return command.to_s if command.to_s.include?(".")
352
476
 
@@ -354,9 +478,19 @@ module Hecks
354
478
  disambiguate_qualified_name(candidates, command, kind, bluebooks)
355
479
  end
356
480
 
357
- # THE SEARCH — every (bluebook, aggregate) pair that declares a
481
+ # Finds every aggregate that could be what a bare command/query name refers to.
482
+ #
483
+ # **The search** — every (bluebook, aggregate) pair that declares a
358
484
  # command/query named `command`, narrowed to `on_aggregate` by name
359
485
  # when given.
486
+ #
487
+ # @param command [String, Symbol] the bare verb to search for
488
+ # @param on_aggregate [String, Symbol, nil] the aggregate to search, or nil to
489
+ # search every aggregate of every bluebook
490
+ # @param bluebooks [Array<Bluebook::Chapter>] every chapter the suite booted
491
+ # @param kind [Symbol] `:command` or `:query`
492
+ # @return [Array<Array(Bluebook::Chapter, Bluebook::Aggregate)>] every matching
493
+ # (chapter, aggregate) pair
360
494
  def qualify_candidates(command, on_aggregate, bluebooks, kind)
361
495
  members = kind == :query ? :queries : :commands
362
496
  pairs =
@@ -368,9 +502,20 @@ module Hecks
368
502
  pairs.select { |_, agg| agg.public_send(members).any? { |m| m.hecks_name == command.to_s } }
369
503
  end
370
504
 
371
- # THE REPORT — zero candidates and more-than-one candidates both
505
+ # Resolves a search's candidates to exactly one dotted FQN, or refuses.
506
+ #
507
+ # **The report** — zero candidates and more-than-one candidates both
372
508
  # refuse (with a different message); exactly one resolves to its
373
509
  # dotted FQN.
510
+ #
511
+ # @param candidates [Array<Array(Bluebook::Chapter, Bluebook::Aggregate)>] the
512
+ # matching (chapter, aggregate) pairs found by `qualify_candidates`
513
+ # @param command [String, Symbol] the bare verb that was searched for
514
+ # @param kind [Symbol] `:command` or `:query`, for the refusal message
515
+ # @param bluebooks [Array<Bluebook::Chapter>] every chapter the suite booted,
516
+ # for the refusal message
517
+ # @return [String] the one candidate's dotted FQN
518
+ # @raise [ArgumentError] if `candidates` is empty, or holds more than one
374
519
  def disambiguate_qualified_name(candidates, command, kind, bluebooks)
375
520
  case candidates.size
376
521
  when 0
@@ -386,8 +531,24 @@ module Hecks
386
531
  end
387
532
  end
388
533
 
534
+ # Builds a passing result.
535
+ #
536
+ # @param test [Behaviors::TestCase] the test that passed
537
+ # @return [Result] a `:pass` result
389
538
  def pass_result(test) = Result.new(description: test.description, status: :pass, message: nil)
539
+
540
+ # Builds a failing result.
541
+ #
542
+ # @param test [Behaviors::TestCase] the test whose expectation was not met
543
+ # @param message [String] what was expected versus what happened
544
+ # @return [Result] a `:fail` result
390
545
  def fail_result(test, message) = Result.new(description: test.description, status: :fail, message: message)
546
+
547
+ # Builds an errored result.
548
+ #
549
+ # @param test [Behaviors::TestCase] the test that could not run to a conclusion
550
+ # @param message [String] what went wrong
551
+ # @return [Result] an `:error` result
391
552
  def error_result(test, message) = Result.new(description: test.description, status: :error, message: message)
392
553
 
393
554
  Result = Struct.new(:description, :status, :message, keyword_init: true)
@@ -2,24 +2,35 @@
2
2
  #
3
3
  # The `.behaviors` authoring surface's own IR — plain Structs, holding
4
4
  # exactly what `Hecks.behaviors "Name" do ... end` collected. Deliberately
5
- # NOT part of `Hecks::IR`/`emits_ir` — a behaviors suite is never
5
+ # not part of `Hecks::IR`/`emits_ir` — a behaviors suite is never
6
6
  # collected into a domain Registry (see `Hecks.behaviors`), never
7
7
  # dispatched through MetaValidator, and carries none of the self-hosted
8
8
  # round-trip machinery a real bluebook construct does. It is a test
9
9
  # artifact a runner reads on demand, not a domain a boot needs.
10
10
  module Hecks
11
+ # The `.behaviors` toolkit: the DSL a `.behaviors` file is written against
12
+ # (`Hecks.behaviors`, dsl.rb), the plain-Struct IR it builds (this file), the runner
13
+ # that boots a suite's domain and checks each test (runner.rb, expectations.rb), and
14
+ # an rspec shim for a consuming app's own suite (rspec.rb).
11
15
  module Behaviors
12
16
  TestSetup = Struct.new(:command, :args, keyword_init: true)
13
17
 
14
18
  TestCase = Struct.new(:description, :tests_command, :on_aggregate, :kind,
15
19
  :setups, :input, :expect, keyword_init: true) do
20
+ # Tells whether `tests_command` is already a fully qualified verb name.
21
+ #
16
22
  # An already-dotted tests_command is a literal FQN; otherwise `on:`
17
23
  # composes with the domain name resolved once setups/the tested
18
24
  # dispatch actually run (see Expectations — the domain isn't known
19
25
  # until `loads` is booted, so it's a runtime concern, not a field
20
26
  # here).
27
+ #
28
+ # @return [Boolean] true when `tests_command` contains a `.`
21
29
  def dotted? = tests_command.to_s.include?(".")
22
30
 
31
+ # Tells whether this test case exercises a query rather than a command.
32
+ #
33
+ # @return [Boolean] true when `kind` is `:query`
23
34
  def query? = kind == :query
24
35
  end
25
36
 
@@ -4,7 +4,7 @@ require_relative "../behaviors"
4
4
  # `bundle exec rspec` uses to run `.behaviors` files as ordinary examples,
5
5
  # one `it` per test, named by the test's own description string. Same
6
6
  # shape `spec/guides_spec.rb` uses for doctested guides: the file is
7
- # PARSED at collection time (cheap — `Behaviors.parse`, no test actually
7
+ # parsed at collection time (cheap — `Behaviors.parse`, no test actually
8
8
  # run yet, just enough to know the `it` names), and each test's own
9
9
  # `Expectations.run_one` runs lazily inside its own `it`, exactly when
10
10
  # rspec actually executes it.
@@ -21,6 +21,14 @@ module Hecks
21
21
  module RSpec
22
22
  module_function
23
23
 
24
+ # Parses one `.behaviors` file and registers an rspec example group for it, one
25
+ # `it` per test named by the test's own description.
26
+ #
27
+ # @param path [String] the `.behaviors` file's path
28
+ # @return [void]
29
+ # @raise [RuntimeError] not raised by this call itself; wraps `parsed.parse_error`'s
30
+ # message and is raised only when rspec later runs the generated "loads without a
31
+ # parse error" example, so it surfaces as that example's failure
24
32
  def describe_file(path)
25
33
  parsed = Behaviors.parse(path)
26
34