hecks 2.7.0 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (463) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +11 -0
  3. data/lib/hecks/adapters/driven/claude_code.rb +4 -104
  4. data/lib/hecks/adapters/driven/d1.rb +44 -237
  5. data/lib/hecks/adapters/driven/folder.rb +17 -76
  6. data/lib/hecks/adapters/driven/google_authentication.rb +15 -49
  7. data/lib/hecks/adapters/driven/governance_authorization.rb +23 -70
  8. data/lib/hecks/adapters/driven/heki/journal.rb +8 -41
  9. data/lib/hecks/adapters/driven/heki/saga_store.rb +12 -54
  10. data/lib/hecks/adapters/driven/heki/snapshot.rb +4 -12
  11. data/lib/hecks/adapters/driven/heki.rb +13 -42
  12. data/lib/hecks/adapters/driven/identity_registry.rb +3 -14
  13. data/lib/hecks/adapters/driven/in_memory_ordering.rb +9 -26
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +2 -14
  15. data/lib/hecks/adapters/driven/lambda/client.rb +10 -51
  16. data/lib/hecks/adapters/driven/lambda.rb +10 -84
  17. data/lib/hecks/adapters/driven/local_storage.rb +5 -40
  18. data/lib/hecks/adapters/driven/memory.rb +18 -99
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +5 -21
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +6 -28
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +3 -12
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +4 -30
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +34 -108
  24. data/lib/hecks/adapters/driven/postgres.rb +145 -134
  25. data/lib/hecks/adapters/driven/postgres_era.adapter +4 -15
  26. data/lib/hecks/adapters/driven/prism.rb +7 -30
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +21 -121
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +8 -40
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +26 -82
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +40 -84
  31. data/lib/hecks/adapters/driven/sqlite.rb +128 -65
  32. data/lib/hecks/adapters/driven/tenant_provisioner.rb +15 -37
  33. data/lib/hecks/adapters/driven.rb +5 -36
  34. data/lib/hecks/adapters/driving/github_webhook.rb +8 -76
  35. data/lib/hecks/behaviors/dsl.rb +7 -25
  36. data/lib/hecks/behaviors/expectations.rb +32 -142
  37. data/lib/hecks/behaviors/ir.rb +7 -20
  38. data/lib/hecks/behaviors/rspec.rb +6 -17
  39. data/lib/hecks/behaviors/runner.rb +10 -34
  40. data/lib/hecks/behaviors.rb +4 -8
  41. data/lib/hecks/bluebook/aggregate.rb +9 -75
  42. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +9 -38
  43. data/lib/hecks/bluebook/assembly/build.rb +4 -15
  44. data/lib/hecks/bluebook/assembly/contract.rb +15 -136
  45. data/lib/hecks/bluebook/assembly/contracts.rb +54 -174
  46. data/lib/hecks/bluebook/assembly/marks.rb +31 -223
  47. data/lib/hecks/bluebook/assembly/specializer.rb +6 -52
  48. data/lib/hecks/bluebook/assembly.rb +6 -45
  49. data/lib/hecks/bluebook/attribute.rb +12 -61
  50. data/lib/hecks/bluebook/behaviour/aggregate.rb +10 -39
  51. data/lib/hecks/bluebook/behaviour/attribute.rb +3 -14
  52. data/lib/hecks/bluebook/behaviour/chapter.rb +17 -42
  53. data/lib/hecks/bluebook/behaviour/command.rb +14 -66
  54. data/lib/hecks/bluebook/behaviour/domain_port.rb +4 -20
  55. data/lib/hecks/bluebook/behaviour/entity.rb +7 -27
  56. data/lib/hecks/bluebook/behaviour/hexagon.rb +23 -41
  57. data/lib/hecks/bluebook/behaviour/lifecycle.rb +15 -40
  58. data/lib/hecks/bluebook/behaviour/policy.rb +23 -52
  59. data/lib/hecks/bluebook/behaviour/process_manager.rb +30 -57
  60. data/lib/hecks/bluebook/behaviour/read_model.rb +38 -26
  61. data/lib/hecks/bluebook/behaviour/traits.rb +27 -64
  62. data/lib/hecks/bluebook/behaviour/value_object.rb +5 -24
  63. data/lib/hecks/bluebook/capabilities.rb +17 -49
  64. data/lib/hecks/bluebook/chapter.rb +10 -48
  65. data/lib/hecks/bluebook/command.rb +23 -81
  66. data/lib/hecks/bluebook/domain_port.rb +14 -63
  67. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +50 -229
  68. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +50 -377
  69. data/lib/hecks/bluebook/dsl/attribute_collector.rb +33 -204
  70. data/lib/hecks/bluebook/dsl/binding_proxy.rb +17 -65
  71. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +86 -708
  72. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +56 -170
  73. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +1 -0
  74. data/lib/hecks/bluebook/dsl/command_builder.rb +74 -546
  75. data/lib/hecks/bluebook/dsl/const_shim.rb +8 -41
  76. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +12 -87
  77. data/lib/hecks/bluebook/dsl/entity_builder.rb +44 -330
  78. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +20 -269
  79. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +26 -189
  80. data/lib/hecks/bluebook/dsl/identity_declaration.rb +13 -88
  81. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +6 -20
  82. data/lib/hecks/bluebook/dsl/policy_builder.rb +16 -88
  83. data/lib/hecks/bluebook/dsl/port_builder.rb +2 -6
  84. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +22 -96
  85. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +31 -184
  86. data/lib/hecks/bluebook/dsl/query_builder.rb +11 -42
  87. data/lib/hecks/bluebook/dsl/read_model_builder.rb +75 -201
  88. data/lib/hecks/bluebook/dsl/rule_reference.rb +22 -163
  89. data/lib/hecks/bluebook/dsl/translation_builder.rb +28 -114
  90. data/lib/hecks/bluebook/dsl/value_object_builder.rb +10 -124
  91. data/lib/hecks/bluebook/dsl/word_gate.rb +16 -162
  92. data/lib/hecks/bluebook/dsl/world_builder.rb +19 -106
  93. data/lib/hecks/bluebook/entity.rb +13 -71
  94. data/lib/hecks/bluebook/expression/ast_json.rb +24 -190
  95. data/lib/hecks/bluebook/expression/ast_reader.rb +12 -54
  96. data/lib/hecks/bluebook/expression/canonical_form.rb +11 -88
  97. data/lib/hecks/bluebook/expression/evaluator.rb +47 -295
  98. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +30 -147
  99. data/lib/hecks/bluebook/expression/resolver.rb +37 -660
  100. data/lib/hecks/bluebook/hexagon.rb +5 -28
  101. data/lib/hecks/bluebook/lifecycle.rb +5 -16
  102. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -7
  103. data/lib/hecks/bluebook/meta_validator/judge.rb +111 -361
  104. data/lib/hecks/bluebook/meta_validator/plan.rb +33 -162
  105. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -7
  106. data/lib/hecks/bluebook/meta_validator/readings.rb +56 -342
  107. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -176
  108. data/lib/hecks/bluebook/meta_validator/shapes.rb +54 -268
  109. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +47 -308
  110. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +7 -43
  111. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -18
  112. data/lib/hecks/bluebook/meta_validator.rb +79 -349
  113. data/lib/hecks/bluebook/model_check/client_profile.rb +10 -91
  114. data/lib/hecks/bluebook/model_check.rb +102 -600
  115. data/lib/hecks/bluebook/pattern_subset.rb +11 -69
  116. data/lib/hecks/bluebook/process_manager.rb +13 -83
  117. data/lib/hecks/bluebook/project_loader.rb +3 -39
  118. data/lib/hecks/bluebook/project_register.rb +12 -60
  119. data/lib/hecks/bluebook/query.rb +9 -48
  120. data/lib/hecks/bluebook/read_model.rb +57 -81
  121. data/lib/hecks/bluebook/reference.rb +7 -41
  122. data/lib/hecks/bluebook/smoke_test.rb +8 -98
  123. data/lib/hecks/bluebook/synthesizer.rb +13 -62
  124. data/lib/hecks/bluebook/translation.rb +26 -62
  125. data/lib/hecks/bluebook/value_object.rb +11 -36
  126. data/lib/hecks/bluebook.rb +5 -22
  127. data/lib/hecks/cache_dir.rb +84 -0
  128. data/lib/hecks/cli/document.rb +86 -0
  129. data/lib/hecks/cli/ir.rb +45 -0
  130. data/lib/hecks/cli/mcp.rb +31 -0
  131. data/lib/hecks/cli/mcp_door.rb +366 -0
  132. data/lib/hecks/cli/model_check.rb +203 -0
  133. data/lib/hecks/cli/project_cli.rb +90 -0
  134. data/lib/hecks/cli/project_diagrams.rb +36 -0
  135. data/lib/hecks/cli/run.rb +173 -0
  136. data/lib/hecks/cli/smoke_test.rb +45 -0
  137. data/lib/hecks/cli/stores.rb +53 -0
  138. data/lib/hecks/cli.rb +160 -0
  139. data/lib/hecks/construct.rb +8 -34
  140. data/lib/hecks/deploy/bluebook/deploy.bluebook +33 -142
  141. data/lib/hecks/deploy/bluebook/deploy.hecksagon +2 -9
  142. data/lib/hecks/embryonaut_bluebook/lock.rb +3 -35
  143. data/lib/hecks/embryonaut_bluebook/shape.rb +1 -17
  144. data/lib/hecks/embryonaut_bluebook/vendor.rb +8 -53
  145. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +4 -19
  146. data/lib/hecks/embryonaut_bluebook.rb +4 -71
  147. data/lib/hecks/facade/cli_door.rb +24 -106
  148. data/lib/hecks/facade/cli_runner.rb +28 -172
  149. data/lib/hecks/facade/command_request.rb +13 -33
  150. data/lib/hecks/facade/handle.rb +34 -121
  151. data/lib/hecks/facade/json_door.rb +31 -146
  152. data/lib/hecks/facade/surface/aggregate_door.rb +39 -104
  153. data/lib/hecks/facade/surface/chapter.rb +10 -54
  154. data/lib/hecks/facade/surface.rb +8 -23
  155. data/lib/hecks/facade.rb +10 -25
  156. data/lib/hecks/forms/app.rb +15 -70
  157. data/lib/hecks/forms/command_form_renderer.rb +10 -29
  158. data/lib/hecks/forms/field_renderer.rb +17 -156
  159. data/lib/hecks/forms/field_shape.rb +30 -222
  160. data/lib/hecks/forms/html.rb +12 -47
  161. data/lib/hecks/forms/index_renderer.rb +1 -4
  162. data/lib/hecks/forms/page.rb +3 -9
  163. data/lib/hecks/forms/params.rb +14 -60
  164. data/lib/hecks/forms/port_argument.rb +2 -19
  165. data/lib/hecks/forms/query_form_renderer.rb +4 -79
  166. data/lib/hecks/forms/record_renderer.rb +8 -75
  167. data/lib/hecks/forms/record_table.rb +7 -50
  168. data/lib/hecks/forms/reference_options.rb +3 -30
  169. data/lib/hecks/forms/value_object_shape.rb +8 -35
  170. data/lib/hecks/forms.rb +5 -33
  171. data/lib/hecks/fqn.rb +5 -12
  172. data/lib/hecks/framework/bluebook/compliance.bluebook +16 -65
  173. data/lib/hecks/framework/bluebook/console_settings.bluebook +47 -134
  174. data/lib/hecks/framework/bluebook/framework.hecksagon +5 -14
  175. data/lib/hecks/framework/bluebook/governance.bluebook +13 -34
  176. data/lib/hecks/framework/bluebook/identity.bluebook +11 -26
  177. data/lib/hecks/framework/bluebook/privacy.bluebook +8 -33
  178. data/lib/hecks/framework.rb +11 -80
  179. data/lib/hecks/freezer.rb +5 -39
  180. data/lib/hecks/grammar/expression.bluebook +15 -40
  181. data/lib/hecks/grammar/translation.bluebook +3 -6
  182. data/lib/hecks/grammar.rb +16 -41
  183. data/lib/hecks/ir.rb +14 -83
  184. data/lib/hecks/language/adapter.bluebook +2 -7
  185. data/lib/hecks/language/bluebook/aggregate.bluebook +56 -221
  186. data/lib/hecks/language/bluebook/attaches/paging.bluebook +6 -15
  187. data/lib/hecks/language/bluebook/bluebook.bluebook +15 -83
  188. data/lib/hecks/language/bluebook/bluebook.hecksagon +3 -15
  189. data/lib/hecks/language/bluebook/command.bluebook +27 -144
  190. data/lib/hecks/language/bluebook/entity.bluebook +24 -97
  191. data/lib/hecks/language/bluebook/policy.bluebook +16 -67
  192. data/lib/hecks/language/bluebook/process_manager.bluebook +28 -151
  193. data/lib/hecks/language/bluebook/projection.bluebook +126 -60
  194. data/lib/hecks/language/bluebook/query.bluebook +12 -43
  195. data/lib/hecks/language/bluebook/shape.bluebook +23 -73
  196. data/lib/hecks/language/bluebook/syntax.bluebook +51 -262
  197. data/lib/hecks/language/bluebook/vocabulary.bluebook +98 -197
  198. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +3 -10
  199. data/lib/hecks/language/hecksagon/domain_port.bluebook +4 -19
  200. data/lib/hecks/language/hecksagon/hecksagon.bluebook +6 -25
  201. data/lib/hecks/language/hecksagon/port_operation.bluebook +3 -14
  202. data/lib/hecks/language/oidc.json +35 -0
  203. data/lib/hecks/language/port.bluebook +4 -17
  204. data/lib/hecks/language/translation/translation.bluebook +3 -9
  205. data/lib/hecks/language/translation/translation_aggregate.bluebook +5 -24
  206. data/lib/hecks/language/world/wiring.bluebook +1 -3
  207. data/lib/hecks/language/world/world.bluebook +5 -7
  208. data/lib/hecks/literal.rb +8 -50
  209. data/lib/hecks/mcp_door_scope.rb +95 -0
  210. data/lib/hecks/mcp_stdio_guard.rb +4 -41
  211. data/lib/hecks/naming.rb +29 -137
  212. data/lib/hecks/ports/access_control.rb +7 -23
  213. data/lib/hecks/ports/agent/answers.rb +5 -23
  214. data/lib/hecks/ports/agent.rb +47 -172
  215. data/lib/hecks/ports/authentication.rb +12 -39
  216. data/lib/hecks/ports/authorization.rb +16 -43
  217. data/lib/hecks/ports/clock.rb +5 -45
  218. data/lib/hecks/ports/extraction.rb +7 -12
  219. data/lib/hecks/ports/identity_assignment.rb +8 -29
  220. data/lib/hecks/ports/identity_generation.rb +6 -29
  221. data/lib/hecks/ports/identity_resolution.rb +2 -10
  222. data/lib/hecks/ports/key_vault.rb +9 -45
  223. data/lib/hecks/ports/persistence/append_only.rb +62 -65
  224. data/lib/hecks/ports/persistence/binding_policy.rb +4 -14
  225. data/lib/hecks/ports/persistence/codec_boundary.rb +20 -80
  226. data/lib/hecks/ports/persistence/null_saga_store.rb +4 -20
  227. data/lib/hecks/ports/persistence/plugin.rb +13 -42
  228. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +38 -141
  229. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +15 -115
  230. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +16 -84
  231. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +7 -26
  232. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +3 -22
  233. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +55 -181
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/events.rb +77 -0
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +12 -122
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +28 -128
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +73 -474
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +31 -124
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +45 -219
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +13 -108
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +18 -75
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +9 -49
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +6 -123
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +23 -55
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +14 -72
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +7 -16
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +14 -115
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +13 -45
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +127 -554
  250. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +13 -63
  251. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +2 -17
  252. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +3 -14
  253. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +13 -58
  254. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -17
  255. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +9 -26
  256. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +9 -25
  257. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +22 -122
  258. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +34 -123
  259. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +5 -12
  260. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +3 -9
  261. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -14
  262. data/lib/hecks/ports/persistence/plugins/era.rb +4 -23
  263. data/lib/hecks/ports/persistence/postgres_dump/connection.rb +7 -23
  264. data/lib/hecks/ports/persistence/postgres_dump.rb +4 -35
  265. data/lib/hecks/ports/persistence/remote_runtime.rb +2 -24
  266. data/lib/hecks/ports/persistence/repository_factory.rb +8 -27
  267. data/lib/hecks/ports/persistence/state_codec.rb +23 -103
  268. data/lib/hecks/ports/persistence.rb +7 -14
  269. data/lib/hecks/ports/projection.rb +42 -84
  270. data/lib/hecks/ports/query/in_memory.rb +10 -58
  271. data/lib/hecks/ports/query/ordering.rb +11 -32
  272. data/lib/hecks/ports/query.rb +14 -37
  273. data/lib/hecks/ports.rb +2 -8
  274. data/lib/hecks/projections/bootstrap_table.rb +11 -43
  275. data/lib/hecks/projections/deploy/fargate/assembly.rb +2 -15
  276. data/lib/hecks/projections/deploy/fargate/cdn.rb +1 -48
  277. data/lib/hecks/projections/deploy/fargate/cdn_default.rb +2 -11
  278. data/lib/hecks/projections/deploy/fargate/check.rb +5 -14
  279. data/lib/hecks/projections/deploy/fargate/containers.rb +3 -78
  280. data/lib/hecks/projections/deploy/fargate/extras.rb +3 -44
  281. data/lib/hecks/projections/deploy/fargate/monitoring.rb +0 -44
  282. data/lib/hecks/projections/deploy/fargate/settings.rb +2 -54
  283. data/lib/hecks/projections/deploy/fargate/yaml.rb +13 -58
  284. data/lib/hecks/projections/deploy/fargate.rb +35 -202
  285. data/lib/hecks/projections/deploy/lambda.rb +151 -679
  286. data/lib/hecks/projections/deploy/preview/access_yaml.rb +1 -33
  287. data/lib/hecks/projections/deploy/preview/containers.rb +6 -111
  288. data/lib/hecks/projections/deploy/preview/routing_yaml.rb +4 -56
  289. data/lib/hecks/projections/deploy/preview/script.rb +2 -14
  290. data/lib/hecks/projections/deploy/preview/settings.rb +3 -33
  291. data/lib/hecks/projections/deploy/preview/task_yaml.rb +2 -38
  292. data/lib/hecks/projections/deploy/preview/template.rb +2 -58
  293. data/lib/hecks/projections/deploy/preview.rb +2 -67
  294. data/lib/hecks/projections/deploy/scripts/settings.rb +3 -25
  295. data/lib/hecks/projections/deploy/scripts.rb +4 -57
  296. data/lib/hecks/projections/deploy/shared.rb +47 -146
  297. data/lib/hecks/projections/deploy/smoke.rb +4 -61
  298. data/lib/hecks/projections/deploy/template_diff/comparer.rb +3 -25
  299. data/lib/hecks/projections/deploy/template_diff/loader.rb +2 -11
  300. data/lib/hecks/projections/deploy/template_diff.rb +5 -57
  301. data/lib/hecks/projections/diagrams.rb +20 -582
  302. data/lib/hecks/projections/glossary/html.rb +14 -130
  303. data/lib/hecks/projections/glossary/markdown.rb +8 -69
  304. data/lib/hecks/projections/glossary/mermaid.rb +9 -66
  305. data/lib/hecks/projections/glossary/sensitivity.rb +5 -34
  306. data/lib/hecks/projections/glossary/sentences.rb +14 -159
  307. data/lib/hecks/projections/glossary.rb +30 -222
  308. data/lib/hecks/projections/ir.rb +3 -10
  309. data/lib/hecks/projections/model/deviations.rb +15 -42
  310. data/lib/hecks/projections/model.rb +12 -51
  311. data/lib/hecks/projections/oidc.rb +12 -56
  312. data/lib/hecks/projections/parser_table.rb +9 -28
  313. data/lib/hecks/projections/reference.rb +9 -16
  314. data/lib/hecks/projections/rust_vocabulary.rb +12 -250
  315. data/lib/hecks/projections/shape.rb +3 -29
  316. data/lib/hecks/projections/statements.rb +7 -86
  317. data/lib/hecks/projections/vocabulary.rb +6 -22
  318. data/lib/hecks/projections.rb +2 -16
  319. data/lib/hecks/projector/cli_projector.rb +60 -375
  320. data/lib/hecks/projector/docs_projector.rb +10 -201
  321. data/lib/hecks/projector/exporter.rb +46 -138
  322. data/lib/hecks/projector/ir_projector.rb +2 -9
  323. data/lib/hecks/projector/narrate_projector.rb +15 -189
  324. data/lib/hecks/projector/target.rb +13 -76
  325. data/lib/hecks/projector.rb +30 -121
  326. data/lib/hecks/query_specification/common/comparators.rb +5 -26
  327. data/lib/hecks/query_specification/common/comparison.rb +35 -143
  328. data/lib/hecks/query_specification/common/dsl.rb +8 -24
  329. data/lib/hecks/query_specification/common/null_policy.rb +20 -70
  330. data/lib/hecks/query_specification/common/options.rb +2 -6
  331. data/lib/hecks/query_specification/common/where_clause.rb +2 -10
  332. data/lib/hecks/query_specification/field_path.rb +37 -77
  333. data/lib/hecks/query_specification/hop_path.rb +12 -76
  334. data/lib/hecks/release/gem_pin.rb +2 -26
  335. data/lib/hecks/release/runner/ci_publisher.rb +93 -0
  336. data/lib/hecks/release/runner/commands.rb +66 -0
  337. data/lib/hecks/release/runner/console.rb +52 -0
  338. data/lib/hecks/release/runner/gem_publisher.rb +43 -0
  339. data/lib/hecks/release/runner/git.rb +48 -0
  340. data/lib/hecks/release/runner/npm_publisher.rb +75 -0
  341. data/lib/hecks/release/runner/preflight.rb +104 -0
  342. data/lib/hecks/release/runner/published.rb +55 -0
  343. data/lib/hecks/release/runner/tagger.rb +80 -0
  344. data/lib/hecks/release/runner.rb +215 -0
  345. data/lib/hecks/rendering.rb +4 -26
  346. data/lib/hecks/router/namespace_installer.rb +12 -36
  347. data/lib/hecks/router.rb +4 -8
  348. data/lib/hecks/runtime/aggregate_lock.rb +8 -33
  349. data/lib/hecks/runtime/boot_gates.rb +3 -18
  350. data/lib/hecks/runtime/caller.rb +19 -59
  351. data/lib/hecks/runtime/capability_graph.rb +5 -21
  352. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +16 -78
  353. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +23 -160
  354. data/lib/hecks/runtime/command_interpreter.rb +125 -307
  355. data/lib/hecks/runtime/command_rules/admissibility.rb +95 -366
  356. data/lib/hecks/runtime/command_rules/arithmetic.rb +66 -196
  357. data/lib/hecks/runtime/command_rules/authorization.rb +13 -45
  358. data/lib/hecks/runtime/command_rules/emission.rb +8 -21
  359. data/lib/hecks/runtime/command_rules/references.rb +23 -202
  360. data/lib/hecks/runtime/dependency_planning.rb +30 -102
  361. data/lib/hecks/runtime/dispatcher.rb +82 -241
  362. data/lib/hecks/runtime/entity_element.rb +40 -462
  363. data/lib/hecks/runtime/entity_interpreter.rb +65 -220
  364. data/lib/hecks/runtime/errors.rb +32 -115
  365. data/lib/hecks/runtime/event.rb +3 -25
  366. data/lib/hecks/runtime/identity.rb +18 -154
  367. data/lib/hecks/runtime/instance.rb +18 -150
  368. data/lib/hecks/runtime/interpreting.rb +11 -70
  369. data/lib/hecks/runtime/invocation.rb +28 -164
  370. data/lib/hecks/runtime/loader.rb +48 -186
  371. data/lib/hecks/runtime/outbox.rb +38 -261
  372. data/lib/hecks/runtime/policy_interpreter.rb +38 -200
  373. data/lib/hecks/runtime/port_operation_interpreter.rb +26 -87
  374. data/lib/hecks/runtime/query_interpreter.rb +45 -187
  375. data/lib/hecks/runtime/reaction_invocation.rb +36 -138
  376. data/lib/hecks/runtime/read_model_interpreter.rb +139 -208
  377. data/lib/hecks/runtime/rebuild_sweep.rb +8 -54
  378. data/lib/hecks/runtime/reference_hop.rb +8 -81
  379. data/lib/hecks/runtime/refusal_wording.rb +8 -89
  380. data/lib/hecks/runtime/registry/saga_persistence.rb +10 -102
  381. data/lib/hecks/runtime/registry/verification.rb +52 -225
  382. data/lib/hecks/runtime/registry/world_defaults.rb +4 -53
  383. data/lib/hecks/runtime/registry.rb +103 -244
  384. data/lib/hecks/runtime/remote_dispatcher.rb +37 -152
  385. data/lib/hecks/runtime/routing.rb +4 -15
  386. data/lib/hecks/runtime/saga_interpreter/correlation.rb +14 -61
  387. data/lib/hecks/runtime/saga_interpreter.rb +79 -263
  388. data/lib/hecks/runtime/saga_pending_dispatch.rb +2 -43
  389. data/lib/hecks/runtime/tenant_check.rb +6 -68
  390. data/lib/hecks/runtime/tenant_scope.rb +3 -28
  391. data/lib/hecks/runtime/value/admission.rb +16 -67
  392. data/lib/hecks/runtime/value/coercion.rb +155 -641
  393. data/lib/hecks/runtime/value/entity_list_coercion.rb +26 -219
  394. data/lib/hecks/runtime/value.rb +12 -84
  395. data/lib/hecks/runtime.rb +17 -65
  396. data/lib/hecks/storehouse.rb +146 -567
  397. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +8 -32
  398. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +6 -23
  399. data/lib/hecks/vendoring/git_environment.rb +2 -8
  400. data/lib/hecks/vendoring/git_source.rb +4 -10
  401. data/lib/hecks/vendoring.rb +7 -46
  402. data/lib/hecks/version.rb +3 -14
  403. data/lib/hecks/vocabulary.rb +5 -0
  404. data/lib/hecks.rb +32 -82
  405. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -46
  406. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +11 -80
  407. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +10 -99
  408. metadata +30 -59
  409. data/lib/hecks/bench/cli.rb +0 -74
  410. data/lib/hecks/bench/environment.rb +0 -100
  411. data/lib/hecks/bench/postgres_probe.rb +0 -41
  412. data/lib/hecks/bench/report.rb +0 -116
  413. data/lib/hecks/bench/ruby_runner.rb +0 -151
  414. data/lib/hecks/bench/run.rb +0 -55
  415. data/lib/hecks/bench/rust_runner.rb +0 -165
  416. data/lib/hecks/bench/stats.rb +0 -75
  417. data/lib/hecks/bench/suite.rb +0 -172
  418. data/lib/hecks/bench/workload.rb +0 -137
  419. data/lib/hecks/bench.rb +0 -28
  420. data/lib/hecks/codemod.rb +0 -443
  421. data/lib/hecks/corpus.rb +0 -479
  422. data/lib/hecks/doc/reference.rb +0 -585
  423. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -684
  424. data/lib/hecks/fuzzing/combination_miner.rb +0 -178
  425. data/lib/hecks/fuzzing/concurrent_dispatch.rb +0 -469
  426. data/lib/hecks/fuzzing/coverage_campaign.rb +0 -161
  427. data/lib/hecks/fuzzing/differential.rb +0 -192
  428. data/lib/hecks/fuzzing/domain_generator.rb +0 -871
  429. data/lib/hecks/fuzzing/era_boundary.rb +0 -154
  430. data/lib/hecks/fuzzing/form_census.rb +0 -285
  431. data/lib/hecks/fuzzing/generated_domain_check.rb +0 -171
  432. data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -125
  433. data/lib/hecks/fuzzing/isolated_boot.rb +0 -658
  434. data/lib/hecks/fuzzing/nondeterministic.rb +0 -79
  435. data/lib/hecks/fuzzing/persistence_parity.rb +0 -253
  436. data/lib/hecks/fuzzing/properties/corrections.rb +0 -125
  437. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +0 -688
  438. data/lib/hecks/fuzzing/properties/guards.rb +0 -398
  439. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +0 -399
  440. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +0 -137
  441. data/lib/hecks/fuzzing/properties/outbox.rb +0 -180
  442. data/lib/hecks/fuzzing/properties/querying.rb +0 -260
  443. data/lib/hecks/fuzzing/properties.rb +0 -267
  444. data/lib/hecks/fuzzing/qa_settings.rb +0 -164
  445. data/lib/hecks/fuzzing/replay.rb +0 -930
  446. data/lib/hecks/fuzzing/rotation_priority.rb +0 -114
  447. data/lib/hecks/fuzzing/rust_gap_manifest.rb +0 -139
  448. data/lib/hecks/fuzzing/self_consistency.rb +0 -825
  449. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +0 -532
  450. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +0 -171
  451. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +0 -127
  452. data/lib/hecks/fuzzing/sequence_generator/picker.rb +0 -116
  453. data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +0 -138
  454. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +0 -344
  455. data/lib/hecks/fuzzing/sequence_generator.rb +0 -311
  456. data/lib/hecks/fuzzing/shrinker.rb +0 -309
  457. data/lib/hecks/fuzzing/structural_skips.rb +0 -53
  458. data/lib/hecks/fuzzing/sweep_depth.rb +0 -61
  459. data/lib/hecks/fuzzing/target_capabilities.rb +0 -255
  460. data/lib/hecks/fuzzing/value_generator.rb +0 -340
  461. data/lib/hecks/fuzzing.rb +0 -23
  462. data/lib/hecks/grammar/evolve.rb +0 -524
  463. data/lib/hecks/query_ir.rb +0 -482
@@ -3,157 +3,38 @@ require_relative "model_check/client_profile"
3
3
 
4
4
  module Hecks
5
5
  module Bluebook
6
- # Lightweight formal methods over the IR — the same family as TLA+/
7
- # Alloy/P: every lifecycle is a declared finite state machine and
8
- # every process manager a declared protocol, and both are already
9
- # data, not code, so they can be model-checked rather than merely
10
- # executed. Static analysis only — no bluebook boots twice, no
11
- # runtime is touched — over what meta_validator/judge.rb and the
12
- # builders' own `validate!` methods leave uncovered (an undeclared
13
- # transition target, a dispatch to nowhere, a compensation nothing
14
- # can ever reach).
15
- #
16
- # **The rare property this rests on**: the model is the implementation.
17
- # A checker over TLA+ verifies a spec a human keeps in sync with
18
- # code by hand ; this verifies the same IR the runtime dispatches
19
- # against, so there is no second copy to drift.
6
+ # Static formal checks over the assembled bluebook IR: lifecycles as state
7
+ # machines, process managers as protocols, checked as data without booting a runtime.
20
8
  module ModelCheck
21
9
  Finding = Struct.new(:kind, :severity, :subject, :message, keyword_init: true) do
22
10
  def to_s = "#{severity.to_s.upcase.ljust(7)} #{kind.to_s.ljust(20)} #{subject} — #{message}"
23
11
  end
24
12
 
25
- # **A finding shipped, not silenced** — the coverage-gate idiom, empty
26
- # allowlists enforced both directions (spec/model_check_spec.rb holds
27
- # this exact table: an error the checker reports and this does not
28
- # name is a regression, an entry the checker no longer reports is
29
- # stale and must be deleted). bin/model_check reads this same
30
- # constant, so the tool and the spec can never drift apart.
31
- #
32
- # "banking"/ExternalSettlement — found on the first real run:
33
- # ExternalSettlement declares `ends_on "ExternalTransferSent"`, and
34
- # ExternalTransfer.Send genuinely emits it — the event is real, and
35
- # the aggregate reaches "sent" (its own, separate lifecycle) — but
36
- # the saga's protocol has no `on "ExternalTransferSent"` handler, so
37
- # its own `state "sent"` is unreachable through the chain the
38
- # checker walks, and the saga's own bookkeeping (saga_log, ends_on)
39
- # never closes it. Real domain activity is unaffected; the saga's
40
- # own tracking of it is not. Left named rather than redesigning a
41
- # corpus fixture that is not this checker's to redesign.
42
- # S7, ADR 0025 — an ExternalSettlement finding once lived here as an
43
- # allowlist entry and is gone now, not just quieted: its "sent" state
44
- # was a `state "x"` line never named by any handler's own from:/to:, a
45
- # pure declaration-drift artifact. States are derived from the
46
- # transitions that name them now (ProcessManagerBuilder#derived_
47
- # states), so a state nothing ever transitions into or out of no
48
- # longer exists to be unreachable — the finding that entry allowlisted
49
- # cannot occur any more, by construction.
50
- #
51
- # "banking"/NotifyOnClosure, FlagKeyReturn — gone from here, moved
52
- # to banking. `across "Notifications"` names a domain that does not
53
- # exist anywhere in this repo, deliberately (it exercises the
54
- # undelivered-reaction runtime path — `spec/runtime/policy_spec.rb`,
55
- # "records a reaction it cannot deliver rather than swallowing it").
56
- # That expectation is now declared on the two policies themselves —
57
- # `across "Notifications", expect_undelivered: true` — and
58
- # `expected_undelivered_findings` below holds it in both directions:
59
- # the unknown-target and unacknowledged-relationship findings are
60
- # expected, and a declaration whose target turns out reachable is a
61
- # `stale_undelivered_expectation` error. A domain's own allowance
62
- # lives in its own source, never in a core table keyed by its name.
63
- #
64
- # Pinned empty (spec/model_check_spec.rb), the way `bin/fuzz`'s
65
- # `KNOWN_FUZZ_FINDINGS` is: a finding a domain means to keep belongs
66
- # in that domain's own declaration.
67
- ALLOWED_FINDINGS = {
68
- # QualityControl was the first domain in this corpus to trigger an
69
- # `asks`/`tells` port operation from a `policy`, and once carried
70
- # two entries here for it — both gone now, not just quieted:
71
- #
72
- # `deaf_policy` (ClearOnPass, RefuseOnFail, RecordTheIssue,
73
- # RecordTheRefusal) went first: `emitted_events` below now reads an
74
- # outbound operation's `.answers`/`.refuses` the same way it already
75
- # read a command's `.emits`, so `Clearance.SuitePassed`/`SuiteFailed`
76
- # and `Ticket.IssueFiled`/`IssueFilingRefused` enter the known-emits
77
- # set for real — the same fix `bin/qa_pr_check`'s own move to
78
- # dispatching through the CI port (rather than `Clearance::Passed`/
79
- # `Failed` directly) needed to make these two policies actually fire.
80
- #
81
- # `unknown_trigger` (FileWhenSubmitted, AskOnceMore) — BUG#23 — was
82
- # never actually a `Naming`/`PolicyBuilder` defect, confirmed by
83
- # tracing the real dispatch path rather than trusting a stale
84
- # comment that once sat here: `Naming.command_ref`'s bare-constant
85
- # rewrite does leave `trigger Ticket::IssueTracker::File` (aggregate,
86
- # port, operation) as "Ticket::IssueTracker.File", a leftover `::`
87
- # past the aggregate — but `PolicyInterpreter#deliver` re-qualifies
88
- # every trigger with this domain's own name before dispatch
89
- # ("QualityControl::Ticket::IssueTracker.File"), and `Naming.
90
- # split_verb` already folds that reintroduced `::` into the
91
- # dot-joined tail correctly (the same fix `ReactionInvocation#
92
- # resolve_target` already carries) — confirmed live: a real
93
- # dispatch through `Ticket.Submit` fires
94
- # `IssueFiled`/`TicketFiled` exactly as declared. The actual gap was
95
- # entirely in this checker: `verbs_of` never enumerated a port
96
- # operation as a triggerable verb at all, and `policy_findings`
97
- # compared raw strings instead of `Naming.split_verb` triples the
98
- # way `handler_findings`'s own `unknown_dispatch` check already does
99
- # (BUG#6). Fixed with `port_verbs_of`/`triggerable_verbs`, scoped
100
- # entirely to this file — no change to `Naming` or `PolicyBuilder`
101
- # was needed or made.
102
- }.freeze
13
+ # Findings a domain deliberately allows, enforced in both directions by
14
+ # spec/model_check_spec.rb: an error this checker reports but isn't listed
15
+ # here is a regression; an entry listed but never reported is stale. Pinned
16
+ # empty — a domain's own allowance belongs on its own declaration. (ADR 0025)
17
+ ALLOWED_FINDINGS = {}.freeze
103
18
 
104
19
  # The profiles `call` accepts; `nil` is the default, unprofiled run.
105
20
  PROFILES = %i[client].freeze
106
21
 
107
22
  module_function
108
23
 
109
- # Runs every static model check over `bluebook` and returns what it
110
- # finds.
111
- #
112
- # `hecksagon:`/`known_domains:` — both optional, both `nil`-safe
113
- # (every existing caller with no sibling hecksagon, or checking one
114
- # domain in isolation, behaves exactly as before). `hecksagon` is
115
- # this bluebook's own sibling wiring file, if the caller loaded one
116
- # (see `emitted_events`'s own comment on why a caller that didn't
117
- # simply finds none, correctly). `known_domains` is the caller's
118
- # own corpus-wide view — every bluebook/hecksagon name it has
119
- # booted anywhere, across every domain it has looked at, not just
120
- # this one — used only to catch a typo'd `across`/`uses_framework`
121
- # target; see `cross_domain_policy_findings`'s own comment for why
122
- # this can only ever be a corpus-scoped heuristic, never a general
123
- # correctness guarantee.
124
- #
125
- # `rust_target:`/`strict:` — both default false, both only change the
126
- # severity of `rust_reserved_name` findings (see
127
- # `rust_reserved_name_findings`); every other finding is unaffected.
24
+ # Runs every static model check over `bluebook` and returns what it finds.
128
25
  #
129
26
  # @param bluebook [Bluebook::Chapter] the assembled chapter to check
130
- # @param hecksagon [Bluebook::Hecksagon, nil] this bluebook's own
131
- # sibling wiring file, if the caller loaded one
132
- # @param known_domains [Set<String>, nil] every bluebook/hecksagon
133
- # name the caller has booted anywhere in this corpus scan
134
- # @param global_emitted_events [Set<String>, nil] every bare event
135
- # name any domain the caller has booted this run emits — lets a
136
- # same-domain-target policy (e.g. one built by `translates`)
137
- # reacting to a genuinely foreign event avoid a false `deaf_policy`
138
- # finding; `nil` (every pre-existing call site) keeps today's
139
- # single-domain-only check
140
- # @param rust_target [Boolean] whether this domain has a real Rust
141
- # target, raising `rust_reserved_name` findings to error
142
- # @param strict [Boolean] whether to raise every `rust_reserved_name`
143
- # finding to error regardless of `rust_target`
144
- # @param profile [Symbol, nil] `:client` adds `ClientProfile`'s error
145
- # findings for constructs known to answer wrongly without refusing;
146
- # `nil` (the default) adds nothing and leaves every other finding as
147
- # it was
148
- # @param translations [Array<Bluebook::Translation>] the data
149
- # translations declared for this chapter's domain, read only by the
150
- # `:client` profile — they live on the registry, not the chapter, so
151
- # the caller supplies them the way it supplies `hecksagon`
152
- # @return [Array<Finding>] every finding this bluebook triggers,
153
- # across its lifecycles, sagas, policies, and Rust-reserved names
154
- # @raise [ArgumentError] if `profile` is neither `nil` nor `:client`
27
+ # @param hecksagon [Bluebook::Hecksagon, nil] this bluebook's own sibling wiring
28
+ # file, if the caller loaded one
29
+ # @param known_domains [Set<String>, nil] every bluebook/hecksagon name booted
30
+ # anywhere in this corpus scan, checked to catch a typo'd across/uses_framework target
31
+ # @param global_emitted_events [Set<String>, nil] every event emitted anywhere in
32
+ # the corpus, checked as a fallback so a translates reaction isn't flagged deaf
33
+ # @param profile [Symbol, nil] :client adds ClientProfile's error findings
34
+ # @return [Array<Finding>] every finding this bluebook triggers
35
+ # @raise [ArgumentError] if profile is neither nil nor :client
155
36
  def call(bluebook, hecksagon: nil, known_domains: nil, global_emitted_events: nil, rust_target: false, strict: false,
156
- profile: nil, translations: [])
37
+ profile: nil)
157
38
  unless profile.nil? || PROFILES.include?(profile)
158
39
  raise ArgumentError, "unknown profile #{profile.inspect} (known: #{PROFILES.inspect})"
159
40
  end
@@ -170,45 +51,20 @@ module Hecks
170
51
  findings.concat(rust_reserved_name_findings(domain_name: bluebook.name,
171
52
  aggregate_names: bluebook.aggregates.map(&:hecks_name),
172
53
  rust_target: rust_target, strict: strict))
173
- findings.concat(ClientProfile.call(bluebook, hecksagon: hecksagon, translations: translations)) if profile == :client
54
+ findings.concat(ClientProfile.call(bluebook, hecksagon: hecksagon)) if profile == :client
174
55
  findings
175
56
  end
176
57
 
177
- # ── Rust reserved names ───────────────────────────────────────────
178
- #
179
- # A name that becomes a bare Rust module identifier with no `r#`
180
- # escape hatch: an aggregate (`pub mod <name.downcase>;` plus its
181
- # `<name.downcase>.rs` file) and a domain (`pub mod <name>;` and a
182
- # Cargo `[features]` key). Field names are not checked — both
183
- # generators already raw-escape those (`rust_ident_field`).
58
+ # Flags an aggregate or domain name that collides with a Rust keyword or reserved
59
+ # Cargo key — both become bare module identifiers with no `r#` escape hatch.
184
60
  #
185
- # The words come from the `RustReservedWord`/`CargoReservedName`
186
- # vocabularies, the same tables `rust/project/naming.rb` and
187
- # hecks-codegen's generated `reserved_names.rs` read. Both Rust
188
- # generators refuse through this check (`Projector.
189
- # reserved_name_refusal`, and its hecks-codegen port in `naming.rs`).
190
- #
191
- # Severity: a domain that only ever runs in Ruby is fine with an
192
- # aggregate named `Match`, so this warns by default. It is an error
193
- # when the caller says the domain has a Rust target (`rust_target:` —
194
- # `bin/model_check` reads it off the domain's Cargo feature, the
195
- # generators always pass it) or asks for strictness (`strict:`,
196
- # `bin/model_check --strict`).
197
- #
198
- # The module-name transform is `downcase`, the one both generators
199
- # apply to an aggregate name and to an attached chapter's name.
200
- #
201
- # @param domain_name [String, Symbol, nil] the domain's own name, or
202
- # `nil` to skip the domain-level check
203
- # @param aggregate_names [Array<String, Symbol>] every aggregate name
204
- # to check
205
- # @param rust_target [Boolean] whether this domain has a real Rust
206
- # target, raising severity to error
207
- # @param strict [Boolean] whether to raise severity to error
208
- # regardless of `rust_target`
209
- # @return [Array<Finding>] one `:rust_reserved_name` finding per
210
- # aggregate or domain name that collides with a Rust or Cargo
211
- # reserved word
61
+ # @param domain_name [String, Symbol, nil] the domain's own name, or nil to skip
62
+ # the domain-level check
63
+ # @param aggregate_names [Array<String, Symbol>] every aggregate name to check
64
+ # @param rust_target [Boolean] raises severity to error when this domain has a
65
+ # real Rust target
66
+ # @param strict [Boolean] raises severity to error regardless of rust_target
67
+ # @return [Array<Finding>] one :rust_reserved_name finding per colliding name
212
68
  def rust_reserved_name_findings(domain_name: nil, aggregate_names: [], rust_target: false, strict: false)
213
69
  severity = rust_target || strict ? :error : :warning
214
70
  keywords = Hecks::Vocabulary.fetch("RustReservedWord")
@@ -225,17 +81,7 @@ module Hecks
225
81
  findings
226
82
  end
227
83
 
228
- # Checks the domain's own module/Cargo-feature name, one level up
229
- # from `rust_reserved_name_findings`'s own per-aggregate check.
230
- #
231
- # @param domain_name [String, Symbol] the domain's own name
232
- # @param keywords [Array<String>] the admitted `RustReservedWord`
233
- # vocabulary rows
234
- # @param severity [Symbol] the finding severity to use, `:error` or
235
- # `:warning`
236
- # @return [Array<Finding>] a single-element array holding a
237
- # `:rust_reserved_name` finding, or `[]` if `domain_name` collides
238
- # with no reserved word or Cargo key
84
+ # The domain-level half of rust_reserved_name_findings — same tables, one name.
239
85
  def domain_reserved_name_findings(domain_name, keywords, severity)
240
86
  module_name = rust_module_name(domain_name)
241
87
  table = if keywords.include?(module_name)
@@ -250,27 +96,10 @@ module Hecks
250
96
  "rename the domain")]
251
97
  end
252
98
 
253
- # Derives the Rust module identifier both generators derive from a
254
- # name.
255
- #
256
- # @param name [String, Symbol] an aggregate or domain name
257
- # @return [String] the Rust module identifier both generators derive
258
- # from `name`
259
99
  def rust_module_name(name) = name.to_s.downcase
260
100
 
261
- # ── lifecycles (aggregate and entity — a piece may declare one too) ──
262
-
263
- # Runs every lifecycle-shaped model check for one declaring
264
- # construct's own lifecycle, if it has one.
265
- #
266
- # @param aggregate [Bluebook::Aggregate] the root aggregate `declaring`
267
- # belongs to
268
- # @param declaring [Bluebook::Aggregate, Bluebook::Entity] the
269
- # construct whose own lifecycle to check — `aggregate` itself, or
270
- # one of its entities
271
- # @return [Array<Finding>] `[]` if `declaring` has no lifecycle;
272
- # otherwise every `unknown_command`/`unreachable_state`/
273
- # `dead_transition`/`stuck_state` finding its lifecycle triggers
101
+ # Runs every lifecycle-shaped check for one declaring construct — the aggregate
102
+ # itself, or one of its entities — if it declares a lifecycle at all.
274
103
  def lifecycle_findings(aggregate, declaring)
275
104
  lifecycle = declaring.lifecycle
276
105
  return [] unless lifecycle
@@ -289,15 +118,7 @@ module Hecks
289
118
  findings
290
119
  end
291
120
 
292
- # Finds every declared state a lifecycle's own reachability walk
293
- # never reaches.
294
- #
295
- # @param lifecycle [Bluebook::Lifecycle] the lifecycle being checked
296
- # @param full [Array<String>] every state `full_states` declares
297
- # @param reached [Set<String>] every state `reachable_states` reaches
298
- # @param subject [String] the finding's own subject label
299
- # @return [Array<Finding>] one `:unreachable_state` finding per
300
- # declared state that `reached` does not cover
121
+ # Finds every declared state the lifecycle's own reachability walk never reaches.
301
122
  def unreachable_state_findings(lifecycle, full, reached, subject)
302
123
  (full - reached.to_a).map do |state|
303
124
  Finding.new(kind: :unreachable_state, severity: :error, subject: subject,
@@ -306,13 +127,7 @@ module Hecks
306
127
  end
307
128
  end
308
129
 
309
- # Finds every constrained transition that can never fire.
310
- #
311
- # @param lifecycle [Bluebook::Lifecycle] the lifecycle being checked
312
- # @param reached [Set<String>] every state `reachable_states` reaches
313
- # @param subject [String] the finding's own subject label
314
- # @return [Array<Finding>] one `:dead_transition` finding per
315
- # constrained transition whose `from:` states are all unreached
130
+ # Finds every constrained transition whose from: states are all unreached.
316
131
  def dead_transition_findings(lifecycle, reached, subject)
317
132
  lifecycle.transitions.filter_map do |command, transition|
318
133
  next unless transition.constrained?
@@ -324,14 +139,8 @@ module Hecks
324
139
  end
325
140
  end
326
141
 
327
- # Finds every reached state with no transition ever leaving it.
328
- #
329
- # @param lifecycle [Bluebook::Lifecycle] the lifecycle being checked
330
- # @param reached [Set<String>] every state `reachable_states` reaches
331
- # @param subject [String] the finding's own subject label
332
- # @return [Array<Finding>] one `:stuck_state` finding per reached,
333
- # non-exempt state with no transition ever leaving it (skipped
334
- # entirely when any transition is unconstrained)
142
+ # Finds every reached state with no transition ever leaving it, unless any
143
+ # transition in this lifecycle is unconstrained.
335
144
  def stuck_state_findings(lifecycle, reached, subject)
336
145
  any_unconstrained = lifecycle.transitions.any? { |_, t| !t.constrained? }
337
146
  (reached - terminal_exempt(lifecycle)).filter_map do |state|
@@ -344,15 +153,7 @@ module Hecks
344
153
  end
345
154
  end
346
155
 
347
- # Finds every transition named after a command this construct does
348
- # not declare.
349
- #
350
- # @param lifecycle [Bluebook::Lifecycle] the lifecycle being checked
351
- # @param commands [Array<String>] every command name the declaring
352
- # construct actually declares
353
- # @param subject [String] the finding's own subject label
354
- # @return [Array<Finding>] one `:unknown_command` finding per
355
- # transition named after a command `commands` does not list
156
+ # Finds every transition named after a command the construct doesn't declare.
356
157
  def unknown_transition_commands(lifecycle, commands, subject)
357
158
  lifecycle.transitions.filter_map do |command, _transition|
358
159
  next if commands.include?(command)
@@ -362,14 +163,11 @@ module Hecks
362
163
  end
363
164
  end
364
165
 
365
- # default, every declared target, and every declared from — a
366
- # from-only state (declared nowhere as a target) is real and is
367
- # exactly the hole `Lifecycle#states` leaves: it answers default
368
- # plus targets only.
166
+ # Every state `lifecycle` declares in any role — default, target, or from,
167
+ # unique.
369
168
  #
370
169
  # @param lifecycle [Bluebook::Lifecycle] the lifecycle being checked
371
- # @return [Array<String>] every state `lifecycle` declares, in any
372
- # role (default, target, or from), unique
170
+ # @return [Array<String>] every state lifecycle declares, unique
373
171
  def full_states(lifecycle)
374
172
  (
375
173
  [lifecycle.default] +
@@ -378,21 +176,14 @@ module Hecks
378
176
  ).uniq
379
177
  end
380
178
 
381
- # Least fixpoint from the default state: an unconstrained
382
- # transition always fires, from wherever the machine is ; a
383
- # constrained one fires once any of its named sources is reached.
384
- #
385
- # @param lifecycle [Bluebook::Lifecycle] the lifecycle being checked
386
- # @return [Set<String>] every state reachable from `lifecycle`'s own
387
- # default state
179
+ # Least fixpoint from the default state: an unconstrained transition always
180
+ # fires; a constrained one fires once any of its named sources is reached.
388
181
  def reachable_states(lifecycle)
389
182
  reached = Set.new([lifecycle.default])
390
183
  loop do
391
184
  grown = false
392
- # `transitions` is an Array of [from, transition]-shaped entries
393
- # (Lifecycle#transitions), not a Hash — Style/HashEachMethods'
394
- # `each_value` rewrite assumed otherwise from the block shape
395
- # alone. False positive.
185
+ # `transitions` is an Array of [from, transition] pairs, not a Hash, so
186
+ # Style/HashEachMethods' each_value rewrite is a false positive here.
396
187
  # rubocop:disable-next Style/HashEachMethods
397
188
  lifecycle.transitions.each do |_, transition|
398
189
  next if reached.include?(transition.target)
@@ -406,26 +197,10 @@ module Hecks
406
197
  reached
407
198
  end
408
199
 
409
- # A state with no outgoing declared path at all is exempt from the
410
- # stuck-state warning for a different reason than "it fires an
411
- # unconstrained transition" — the default state of a lifecycle
412
- # with only constrained transitions is legitimately allowed to sit
413
- # forever, since nothing about *entering* it via default implies
414
- # anything must eventually move it, unlike a state a transition
415
- # explicitly delivered somewhere.
416
- #
417
- # Scoped to `default` alone, and only when the lifecycle actually
418
- # declares real transitions elsewhere: an empty lifecycle (no
419
- # transitions at all) doesn't get this exemption — that's not "a
420
- # machine whose entry point deliberately awaits external action,"
421
- # it's much more likely a lifecycle nobody finished wiring, and
422
- # should still warn (see spec/fixtures/model_check/lifecycle_
423
- # findings.bluebook's own Widget::Part, which stays warned on
424
- # purpose).
425
- #
426
- # @param lifecycle [Bluebook::Lifecycle] the lifecycle being checked
427
- # @return [Array<String>] `[lifecycle.default]` when it is exempt from
428
- # the stuck-state warning, `[]` otherwise
200
+ # Exempts only the lifecycle's own default state, and only when the lifecycle
201
+ # declares real transitions elsewhere: entering default implies nothing about
202
+ # ever leaving it, unlike a state some transition explicitly delivered to. An
203
+ # empty lifecycle still warns — that reads as unfinished wiring, not a deliberate rest.
429
204
  def terminal_exempt(lifecycle)
430
205
  return [] if lifecycle.transitions.empty?
431
206
 
@@ -433,24 +208,12 @@ module Hecks
433
208
  outgoing_sources.include?(lifecycle.default) ? [] : [lifecycle.default]
434
209
  end
435
210
 
436
- # ── process managers / sagas ──────────────────────────────────────
437
-
438
- # Runs every saga-shaped model check for one process manager.
439
- #
440
- # @param bluebook [Bluebook::Chapter] the assembled chapter
441
- # `process_manager` belongs to
442
- # @param process_manager [Bluebook::ProcessManager] the process
443
- # manager to check
444
- # @return [Array<Finding>] every `deaf_trigger`/`unreachable_pm_state`/
445
- # `deaf_handler`/`unknown_dispatch`/`unarmed_compensation`/
446
- # `dead_compensation` finding this process manager triggers
211
+ # Runs every saga-shaped check for one process manager.
447
212
  def saga_findings(bluebook, process_manager)
448
213
  emitted = emitted_events(bluebook)
449
- # (domain, aggregate, command) triples, not raw strings — see
450
- # `handler_findings`'s own comment on the dispatch side for why:
451
- # `Naming.split_verb` is what makes an entity verb's two legitimate
452
- # spellings (`Naming.command_ref`'s own `::`-then-`.` rewrite vs.
453
- # `verbs_of`'s own all-`.` one) compare equal.
214
+ # Parsed as (domain, aggregate, command) triples via Naming.split_verb, not
215
+ # compared as raw strings — that's what makes an entity verb's two legitimate
216
+ # spellings compare equal (see handler_findings' unknown_dispatch check).
454
217
  verbs = verbs_of(bluebook).map { |verb| Naming.split_verb(verb) }
455
218
  reached = pm_reachable_states(process_manager, emitted)
456
219
 
@@ -464,15 +227,7 @@ module Hecks
464
227
  findings
465
228
  end
466
229
 
467
- # Finds every declared `starts_on`/`ends_on` event this domain never
468
- # emits.
469
- #
470
- # @param process_manager [Bluebook::ProcessManager] the process
471
- # manager being checked
472
- # @param emitted [Array<String>] every bare event name this domain
473
- # actually emits
474
- # @return [Array<Finding>] one `:deaf_trigger` finding per declared
475
- # `starts_on`/`ends_on` event this domain never emits
230
+ # Finds every declared starts_on/ends_on event this domain never emits.
476
231
  def deaf_trigger_findings(process_manager, emitted)
477
232
  [process_manager.starts_on, process_manager.ends_on].compact.filter_map do |event|
478
233
  next if emitted.include?(bare(event))
@@ -484,13 +239,6 @@ module Hecks
484
239
  end
485
240
 
486
241
  # Finds every declared state no handler chain ever reaches.
487
- #
488
- # @param process_manager [Bluebook::ProcessManager] the process
489
- # manager being checked
490
- # @param reached [Set<String>] every state `pm_reachable_states`
491
- # reaches
492
- # @return [Array<Finding>] one `:unreachable_pm_state` finding per
493
- # declared state that `reached` does not cover
494
242
  def unreachable_pm_state_findings(process_manager, reached)
495
243
  (Array(process_manager.states) - reached.to_a).map do |state|
496
244
  Finding.new(kind: :unreachable_pm_state, severity: :error, subject: process_manager.name,
@@ -499,33 +247,12 @@ module Hecks
499
247
  end
500
248
  end
501
249
 
502
- # One handler's own findings — deaf_handler, unknown_dispatch (one
503
- # per dispatch), and unarmed_compensation (one per compensating
504
- # dispatch), pulled out of saga_findings' own handler loop; each
505
- # check reads only this handler plus the domain-wide emitted/verbs
506
- # sets saga_findings already resolved once, no state shared between
507
- # handlers.
508
- #
509
- # @param bluebook [Bluebook::Chapter] the assembled chapter
510
- # `process_manager` belongs to
511
- # @param process_manager [Bluebook::ProcessManager] the process
512
- # manager `handler` belongs to
513
- # @param emitted [Array<String>] every bare event name this domain
514
- # actually emits
515
- # @param verbs [Array<Array(String, String, String)>] every command
516
- # this domain declares, parsed as `Naming.split_verb`
517
- # `[domain, aggregate, command]` triples
518
- # @param handler [Bluebook::ProcessManagerHandler] the handler to
519
- # check
520
- # @return [Array<Finding>] every `deaf_handler`/`unknown_dispatch`/
521
- # `unarmed_compensation` finding this handler triggers
250
+ # One handler's own deaf_handler/unknown_dispatch/unarmed_compensation findings.
522
251
  def handler_findings(bluebook, process_manager, emitted, verbs, handler)
523
252
  findings = []
524
253
 
525
- # The compensating leg answers `REFUSED`, a synthetic trigger no
526
- # command ever emits by name (`ProcessManager::REFUSED`) — not
527
- # a deaf handler, the one handler this domain's own events can
528
- # never satisfy on purpose.
254
+ # The compensating leg answers `REFUSED`, a synthetic trigger no command
255
+ # ever emits by name — deliberately exempt from the deaf-handler check.
529
256
  if handler.event_type != ProcessManager::REFUSED && !emitted.include?(bare(handler.event_type))
530
257
  findings << Finding.new(kind: :deaf_handler, severity: :error, subject: process_manager.name,
531
258
  message: "a handler answers #{handler.event_type.inspect}, which no command " \
@@ -533,37 +260,9 @@ module Hecks
533
260
  end
534
261
 
535
262
  handler.dispatches.each do |dispatch|
536
- # **Always this domain** — same fix, same reason, as `SagaInterpreter
537
- # #qualified` (BUG#6). Guessing instead — reading a dispatch whose
538
- # own `command_name` still carries a leftover `::` after `Naming.
539
- # command_ref`'s own rewrite as "already qualified" and leaving it
540
- # alone — would hit the exact same string-shape ambiguity that
541
- # `SagaInterpreter#qualified`'s own comment explains at length
542
- # (a same-domain entity command reference and a genuinely
543
- # cross-domain one are textually indistinguishable after that
544
- # rewrite). Confirmed against the entire corpus, same as that
545
- # fix: no saga anywhere ever dispatches genuinely cross-domain,
546
- # so this checker qualifies exactly the way the runtime
547
- # actually dispatches — unconditionally against `bluebook.name`
548
- # — instead of maintaining its own, independently-wrong copy of
549
- # the same guess.
550
- #
551
- # Compared as a triple, not a string — `Naming.command_ref`'s
552
- # own rewrite of an entity reference (`Manifest::Slot::Fill`)
553
- # collapses to "Manifest::Slot.Fill" (`::` between aggregate and
554
- # entity, `.` before the command); `verbs_of`'s own entity
555
- # spelling, below, joins aggregate/entity/command all with `.`
556
- # instead (matching `fuzzing/sequence_generator/catalog.rb`'s own
557
- # independent convention, its comment's own "the same spelling"
558
- # claim). Both are legitimate, and `Naming.split_verb` already
559
- # parses either to the identical (domain, aggregate, command)
560
- # triple (its own comment: "past the already-resolved domain
561
- # boundary, any leftover `::` is unambiguous... folding it into
562
- # the dot-joined tail") — the same reading `ReactionInvocation.
563
- # resolve_target` relies on at runtime. A bare string `include?`
564
- # would falsely flag every entity dispatch as unknown_dispatch
565
- # even once correctly domain-qualified, comparing two spellings
566
- # of the same verb as though they were different ones.
263
+ # Always qualified against this domain's own name (no saga dispatches
264
+ # cross-domain) and compared as a parsed triple, not a string, so an
265
+ # entity verb's two legitimate spellings compare equal.
567
266
  qualified = Naming.split_verb("#{bluebook.name}::#{dispatch.command_name}")
568
267
  next if verbs.include?(qualified)
569
268
 
@@ -573,14 +272,9 @@ module Hecks
573
272
  "checker's scope, same as CommandRules#resolve_references")
574
273
  end
575
274
 
576
- # A `compensates` declared with nowhere to ever fire — the exact
577
- # shape of the real bug this whole feature closes ("the
578
- # reversal was written and never armed"), caught at build/
579
- # model-check time instead of discovered in production. No
580
- # handler anywhere answers `REFUSED` (`process_manager.saga?` false) means
581
- # `SagaInterpreter#unwind` never runs for this process
582
- # manager at all, so a declared `compensates` is structurally
583
- # unreachable — not a warning about style, a dead declaration.
275
+ # No handler anywhere answers `REFUSED` means SagaInterpreter#unwind never
276
+ # runs for this process manager, so a declared compensates is structurally
277
+ # unreachable — a dead declaration, not a style warning.
584
278
  if !process_manager.saga? && handler.dispatches.any?(&:compensates)
585
279
  handler.dispatches.select(&:compensates).each do |dispatch|
586
280
  findings << Finding.new(kind: :unarmed_compensation, severity: :error, subject: process_manager.name,
@@ -593,16 +287,7 @@ module Hecks
593
287
  findings
594
288
  end
595
289
 
596
- # Checks whether a saga's own compensation leaves an unreachable
597
- # state.
598
- #
599
- # @param process_manager [Bluebook::ProcessManager] the process
600
- # manager being checked
601
- # @param reached [Set<String>] every state `pm_reachable_states`
602
- # reaches
603
- # @return [Array<Finding>] a single-element array holding a
604
- # `:dead_compensation` finding, or `[]` if `process_manager` has no
605
- # saga or its own compensation leaves a reached state
290
+ # Checks whether a saga's own compensation leaves an unreachable state.
606
291
  def dead_compensation_findings(process_manager, reached)
607
292
  return [] unless process_manager.saga? && !reached.include?(process_manager.saga.from_state)
608
293
 
@@ -611,22 +296,10 @@ module Hecks
611
296
  "handler chain ever reaches — a refusal here can never fire it")]
612
297
  end
613
298
 
614
- # A handler edge is only usable in the closure if it can actually
615
- # fire — `REFUSED` always can (it is a compensation trigger, not an
616
- # event), and any other handler needs its event genuinely emitted.
617
- # Without this, a deaf handler's declared from_state -> to_state
618
- # pair reads as connected even though nothing can ever traverse
619
- # it, which would hide exactly the states this walk exists to
620
- # catch (a state only "reachable" through a handler that itself
621
- # never fires).
622
- #
623
- # @param process_manager [Bluebook::ProcessManager] the process
624
- # manager being checked
625
- # @param emitted [Array<String>] every bare event name this domain
626
- # actually emits
627
- # @return [Set<String>] every state reachable from
628
- # `process_manager`'s own first declared state, through handlers
629
- # that can actually fire
299
+ # Only a handler that can actually fire extends the closure — `REFUSED` always
300
+ # can (it's a compensation trigger, not an event); any other handler needs its
301
+ # event genuinely emitted. Otherwise a deaf handler's edge would read as
302
+ # connected even though nothing can ever traverse it.
630
303
  def pm_reachable_states(process_manager, emitted)
631
304
  return Set.new if Array(process_manager.states).empty?
632
305
 
@@ -646,77 +319,25 @@ module Hecks
646
319
  reached
647
320
  end
648
321
 
649
- # ── policies ───────────────────────────────────────────────────────
650
-
651
322
  # Runs every same-domain policy check, or defers to
652
- # `cross_domain_policy_findings` for a policy that targets another
653
- # domain.
654
- #
655
- # @param bluebook [Bluebook::Chapter] the assembled chapter `policy`
656
- # belongs to
657
- # @param policy [Bluebook::Policy] the policy to check
658
- # @param hecksagon [Bluebook::Hecksagon, nil] this bluebook's own
659
- # sibling wiring file, if the caller loaded one
660
- # @param known_domains [Set<String>, nil] every bluebook/hecksagon
661
- # name the caller has booted anywhere in this corpus scan
662
- # @return [Array<Finding>] every `deaf_policy`/`unknown_trigger`
663
- # finding this policy triggers, or `cross_domain_policy_findings`'s
664
- # own return for a cross-domain policy
323
+ # cross_domain_policy_findings for a cross-domain policy.
665
324
  def policy_findings(bluebook, policy, hecksagon, known_domains, global_emitted_events = nil)
666
325
  return cross_domain_policy_findings(policy, hecksagon, known_domains) if policy.target_domain
667
326
 
668
327
  emitted = emitted_events(bluebook)
669
328
  findings = []
670
329
 
671
- # `policy.event_name` (Naming.unqualified) — not `bare`, which only
672
- # strips a "::" domain qualifier. An aggregate-scoped policy's
673
- # `on_event` carries a "." aggregate qualifier instead (PolicyBuilder
674
- # stores whatever was typed, verbatim — see `on "Account.
675
- # AccountFrozen"`), and `bare` left it untouched, silently comparing
676
- # "Account.AccountFrozen" against a list of bare emitted names that
677
- # can never contain it. Latent until now : banking's one aggregate-
678
- # scoped same-domain policy check would have caught it, but its only
679
- # prior aggregate-scoped policy (ReviewOnFreeze) is also cross-domain
680
- # (`across "Compliance"`), which exits this method one line above
681
- # before the mismatch is ever reached.
682
- # `global_emitted_events` — a `translates` (hecksagon-level)
683
- # reaction is BUILT as the exact same same-domain-target `Policy`
684
- # an ordinary `policy` block is (no `target_domain`, so it never
685
- # reaches `cross_domain_policy_findings` above), but its whole
686
- # point is to react to a FOREIGN domain's own event — this
687
- # domain's own `emitted_events(bluebook)` was never going to
688
- # contain it. Checked only as a FALLBACK, after the local check
689
- # already failed, so a real same-domain typo still gets flagged
690
- # exactly as before whenever the caller has no cross-domain set
691
- # to offer (nil — every existing call site, unchanged) or the
692
- # event genuinely isn't emitted anywhere this run examined.
330
+ # `policy.event_name`, not `bare` — an aggregate-scoped on_event carries a
331
+ # "." qualifier bare doesn't strip. `global_emitted_events` is a fallback for
332
+ # a translates reaction, whose whole point is reacting to a foreign event.
693
333
  unless emitted.include?(policy.event_name) || global_emitted_events&.include?(policy.event_name)
694
334
  findings << Finding.new(kind: :deaf_policy, severity: :error, subject: policy.name,
695
335
  message: "on #{policy.on_event.inspect}, which no command in this domain emits")
696
336
  end
697
337
 
698
- # `trigger` is spelled "Aggregate.Command" (or "Entity.Command" one
699
- # level down), completed to an FQN by PolicyInterpreter#deliver as
700
- # "#{domain}::#{trigger_command}" — the same join `verbs_of` builds
701
- # independently, so the two spellings have to be compared as FQNs.
702
- #
703
- # Compared as a triple, not a string — `handler_findings`'s own
704
- # `unknown_dispatch` check (BUG#6) already applies this fix for a
705
- # saga's dispatch; a policy's `trigger` needed the identical one. A
706
- # policy triggering an `asks`/`tells` port operation (`Aggregate::
707
- # Port::Operation`, three colon-joined segments — `Naming.command_ref`'s
708
- # bare-constant rewrite turns this into "Aggregate::Port.Operation",
709
- # a leftover `::` past the aggregate) is a real, working dispatch —
710
- # `PolicyInterpreter#deliver` qualifies it with this domain's own
711
- # name before `Naming.split_verb` ever sees it, and `split_verb`
712
- # already folds that leftover `::` into the dot-joined tail
713
- # correctly (fixed for `ReactionInvocation#resolve_target`, PR
714
- # #520) — but this check compared raw strings against `verbs_of`,
715
- # which never enumerated port operations at all, so it reported
716
- # every port-operation trigger as unknown regardless. `triggerable_
717
- # verbs` now includes both, and both sides are parsed through
718
- # `Naming.split_verb` before comparing, the same reading
719
- # `resolve_target` relies on at runtime.
338
+ # Completed to the same FQN verbs_of builds, then compared as a parsed
339
+ # triple — not a string — so a port-operation trigger's spelling matches
340
+ # the same way an entity command's does (see handler_findings above).
720
341
  qualified = Naming.split_verb("#{bluebook.name}::#{policy.trigger_command}")
721
342
  unless qualified && triggerable_verbs(bluebook).include?(qualified)
722
343
  findings << Finding.new(kind: :unknown_trigger, severity: :error, subject: policy.name,
@@ -727,35 +348,9 @@ module Hecks
727
348
  findings
728
349
  end
729
350
 
730
- # ── cross-domain policies (Context Mapping) ───────────────────────
731
- #
732
- # `uses_framework "X"` already is a Shared Kernel relationship — it
733
- # merges X's own bluebook into this registry, no boundary. A cross-
734
- # domain `policy ... across: "X"` already is a Customer/Supplier
735
- # relationship — it dispatches into X over real cross-Lambda RPC in
736
- # the Rust host (`rust/host/src/lambda_client.rs`). Neither is a new
737
- # word; this makes the choice between them checked instead of a
738
- # prose comment nobody enforces (`examples/banking/bluebook/
739
- # banking.hecksagon`'s own hand-written note explaining why
740
- # Compliance is reached via `across`, never `uses_framework`).
741
- #
742
- # No new keyword anywhere — ADR 0025 principle 1 ("one idea, one
743
- # spelling") refuses a `relationship:`/`as:` argument that would
744
- # just restate, as a string, the fact the chosen keyword (
745
- # `uses_framework` vs `across`) already states completely. The
746
- # DDD vocabulary (Shared Kernel, Customer/Supplier) lives here, in
747
- # the finding's own name and this comment, and in prose docs — not
748
- # in the grammar.
749
- #
750
- # @param policy [Bluebook::Policy] the cross-domain policy to check
751
- # @param hecksagon [Bluebook::Hecksagon, nil] this bluebook's own
752
- # sibling wiring file, if the caller loaded one
753
- # @param known_domains [Set<String>, nil] every bluebook/hecksagon
754
- # name the caller has booted anywhere in this corpus scan
755
- # @return [Array<Finding>] `expected_undelivered_findings`'s own
756
- # return for a policy declared `expect_undelivered`; otherwise every
757
- # `contradictory_relationship`/`unacknowledged_relationship`/
758
- # `unknown_target_domain` finding this policy triggers
351
+ # `uses_framework` is a Shared Kernel relationship (X merges in, no boundary);
352
+ # `across` is Customer/Supplier (real cross-Lambda RPC). This checks the choice
353
+ # between them instead of leaving it as an unenforced comment. (ADR 0025)
759
354
  def cross_domain_policy_findings(policy, hecksagon, known_domains)
760
355
  return expected_undelivered_findings(policy, hecksagon, known_domains) if policy.expect_undelivered
761
356
  # No sibling hecksagon loaded — nothing to check a relationship against.
@@ -765,12 +360,9 @@ module Hecks
765
360
  findings = []
766
361
 
767
362
  if hecksagon.framework_members.include?(target)
768
- # Shared kernel and customer/supplier are mutually exclusive
769
- # claims about the same target — `uses_framework` means "X is
770
- # loaded in-process, right here"; `across` means "X is a
771
- # separate deployment, reached only by RPC." Declaring both is
772
- # either a pointless RPC to a domain already local, or a
773
- # `uses_framework` that isn't really doing what its name says.
363
+ # Shared Kernel and Customer/Supplier are mutually exclusive claims about
364
+ # the same target — declaring both means either a pointless RPC to a
365
+ # domain already local, or a uses_framework that isn't doing its job.
774
366
  findings << Finding.new(kind: :contradictory_relationship, severity: :error, subject: policy.name,
775
367
  message: "across #{target.inspect} dispatches over RPC (Customer/Supplier), " \
776
368
  "but this hecksagon also uses_framework #{target.inspect} (Shared " \
@@ -778,11 +370,8 @@ module Hecks
778
370
  "relationship declarations contradict each other for the same " \
779
371
  "target domain")
780
372
  elsif hecksagon.subscriptions.none? { |subscribed| Naming.qualifier(subscribed) == target }
781
- # This is what finally gives `subscribe` real teeth — checked
782
- # here, at model-check time, still never routed at runtime
783
- # (nothing dispatches off a `subscribe` line; see hecksagon.md's
784
- # own "checked, not routed" section). ADR 0025 names `subscribe`
785
- # by number as failing the corpus-use bar; this is the real use.
373
+ # Checked here at model-check time; subscribe itself is never routed at
374
+ # runtime (see hecksagon.md's own "checked, not routed" section).
786
375
  findings << Finding.new(kind: :unacknowledged_relationship, severity: :error, subject: policy.name,
787
376
  message: "across #{target.inspect} declares a Customer/Supplier " \
788
377
  "relationship, but nothing in this hecksagon records the " \
@@ -791,17 +380,9 @@ module Hecks
791
380
  "attach it in-process instead")
792
381
  end
793
382
 
794
- # **Typo detection, deliberately weaker** — `known_domains` can only
795
- # ever be a monorepo-scoped heuristic: a real external hecks
796
- # consumer's own domain (this repo's own embryonaut client-
797
- # shaped case) lives in a genuinely separate repository this
798
- # corpus scan can never see, so a target this check cannot find
799
- # is "unknown to THIS corpus," never proof of a typo. A target
800
- # undefined by design (the corpus's own "Notifications", used
801
- # deliberately to exercise the undelivered-reaction runtime path)
802
- # declares so on its own policy — `expect_undelivered: true`,
803
- # checked by `expected_undelivered_findings` — rather than being
804
- # named in a core allowlist.
383
+ # A heuristic, not proof: an external hecks consumer's own domain lives in a
384
+ # repo this corpus scan can't see, so "unknown here" isn't necessarily a typo.
385
+ # A domain undefined on purpose declares so via expect_undelivered instead.
805
386
  if known_domains && !known_domains.include?(target)
806
387
  findings << Finding.new(kind: :unknown_target_domain, severity: :error, subject: policy.name,
807
388
  message: "across #{target.inspect} names a domain nowhere in the corpus " \
@@ -813,25 +394,10 @@ module Hecks
813
394
  findings
814
395
  end
815
396
 
816
- # A declared undelivered target, held to its declaration. The two
817
- # findings an unreachable `across` target raises (unknown target,
818
- # unacknowledged relationship) are what the policy declared it
819
- # expects, so they are not raised. What is raised is the declaration
820
- # going stale: the target is a domain this corpus actually booted,
821
- # or the sibling hecksagon attaches or subscribes to it — either way
822
- # the reaction can be delivered, and the declaration is now a lie.
823
- # `known_domains` is nil for a single-target run, which can then only
824
- # check the hecksagon half.
825
- #
826
- # @param policy [Bluebook::Policy] the `expect_undelivered` policy to
827
- # check
828
- # @param hecksagon [Bluebook::Hecksagon, nil] this bluebook's own
829
- # sibling wiring file, if the caller loaded one
830
- # @param known_domains [Set<String>, nil] every bluebook/hecksagon
831
- # name the caller has booted anywhere in this corpus scan
832
- # @return [Array<Finding>] a single-element array holding a
833
- # `:stale_undelivered_expectation` finding, or `[]` if the target
834
- # remains genuinely unreachable
397
+ # Holds an expect_undelivered declaration to its word: the two findings an
398
+ # unreachable target would otherwise raise are suppressed (that's the point of
399
+ # the declaration), but if the target turns out reachable after all, the stale
400
+ # declaration itself is now the error.
835
401
  def expected_undelivered_findings(policy, hecksagon, known_domains)
836
402
  target = policy.target_domain
837
403
  reached = []
@@ -850,33 +416,12 @@ module Hecks
850
416
  "what reaches #{target}")]
851
417
  end
852
418
 
853
- # ── shared enumeration ────────────────────────────────────────────
854
-
855
- # A port operation emits too — the primary/driving port an adapter
856
- # outside the bluebook calls through (see hecksagon_builder.rb) is a
857
- # second, real source of events, alongside a command's own `emits`.
858
- # Ports attach to the aggregate/bluebook from the sibling `.hecksagon`
859
- # file, not this one — a caller that boots only the `.bluebook` (as
860
- # the fixtures under spec/fixtures/model_check/ do, having no
861
- # hecksagon at all) simply finds none, which is correct : nothing
862
- # can be deaf to an event that isn't even wired up yet.
863
- #
864
- # An outbound operation (`asks`) emits through a different door — it
865
- # declares no `.emits` at all (`PortOperationBuilder#refuse_wrong_
866
- # words!` refuses one that tries), naming its two real endings
867
- # `.answers`/`.refuses` instead (`PortOperation#initialize`). Reading
868
- # only `.emits` left every `asks`'s own two events invisible to this
869
- # method — real, live events a policy genuinely reacts to
870
- # (`Clearance.SuitePassed`/`SuiteFailed`, `Ticket.IssueFiled`/
871
- # `IssueFilingRefused`), reported as `deaf_policy` findings until this
872
- # read both. `.compact` because an inbound operation's `.answers`/
873
- # `.refuses` are always nil (there is no channel back to tell), which
874
- # would otherwise seed every emitted-events set with a stray nil.
419
+ # Every bare event name this domain's commands and port operations emit,
420
+ # answer, or refuse — an outbound `asks` port operation has no `.emits`.
875
421
  #
876
- # @param bluebook [Bluebook::Chapter] the assembled chapter to
877
- # enumerate
878
- # @return [Array<String>] every bare event name this domain's
879
- # commands and port operations emit, answer, or refuse, unique
422
+ # @param bluebook [Bluebook::Chapter] the assembled chapter to enumerate
423
+ # @return [Array<String>] every bare event name emitted, answered, or refused
424
+ # across this domain, unique
880
425
  def emitted_events(bluebook)
881
426
  aggregate_emits = bluebook.aggregates.flat_map do |aggregate|
882
427
  aggregate.commands.map(&:emits) +
@@ -888,32 +433,14 @@ module Hecks
888
433
  (aggregate_emits + chapter_emits).flatten.compact.uniq
889
434
  end
890
435
 
891
- # One operation, either of its own sources of events — an inbound
892
- # `tells` names its own via `.emits`; an outbound `asks` has none
893
- # (`PortOperationBuilder#refuse_wrong_words!` refuses one that
894
- # tries) and names its two real endings `.answers`/`.refuses`
895
- # instead. Pulled out of `emitted_events` above purely to keep that
896
- # method's own branching low enough to read at a glance — every
897
- # port, aggregate-owned or chapter-level, asks this the same way.
898
- #
899
- # @param ports [Array<Bluebook::DomainPort>] the ports to enumerate
900
- # @return [Array<String, nil>] every event name each operation emits,
901
- # answers, or refuses; `nil` entries included (compacted by the
902
- # caller)
436
+ # One port's own emitted/answered/refused event names — an inbound operation's
437
+ # answers/refuses are always nil, compacted by the caller.
903
438
  def port_operation_events(ports)
904
439
  ports.flat_map { |port| port.operations.flat_map { |op| [*op.emits, op.answers, op.refuses] } }
905
440
  end
906
441
 
907
- # Fully-qualified, the same spelling DispatchSpec#command_name
908
- # carries and fuzzing/sequence_generator/catalog.rb builds
909
- # independently for the same reason: a saga dispatch and a fuzzer
910
- # step both have to name a verb the same way the door does.
911
- #
912
- # @param bluebook [Bluebook::Chapter] the assembled chapter to
913
- # enumerate
914
- # @return [Array<String>] every command's own fully-qualified verb,
915
- # `"Domain::Aggregate.Command"` or
916
- # `"Domain::Aggregate.Entity.Command"`
442
+ # Every command's own fully-qualified verb: "Domain::Aggregate.Command" or
443
+ # "Domain::Aggregate.Entity.Command".
917
444
  def verbs_of(bluebook)
918
445
  bluebook.aggregates.flat_map do |aggregate|
919
446
  verbs = aggregate.commands.map { |command| "#{bluebook.name}::#{aggregate.hecks_name}.#{command.hecks_name}" }
@@ -925,20 +452,9 @@ module Hecks
925
452
  end
926
453
  end
927
454
 
928
- # An aggregate-owned port operation is a triggerable verb too —
929
- # `ReactionInvocation#resolve_target`'s own port-operation branch
930
- # resolves one by the exact same two-segment tail shape ("Aggregate::
931
- # Port.Operation", the aggregate then the port then the operation,
932
- # dot-joined past the domain) an entity command uses, checked first,
933
- # same order `Dispatcher#dispatch` already resolves a live verb in.
934
- # Only an aggregate's own ports (`aggregate.ports`) are in scope here
935
- # — a policy's `trigger` always names one aggregate, never a chapter-
936
- # level port with no owner to address through.
937
- #
938
- # @param bluebook [Bluebook::Chapter] the assembled chapter to
939
- # enumerate
940
- # @return [Array<String>] every aggregate-owned port operation's own
941
- # fully-qualified verb, `"Domain::Aggregate.Port.Operation"`
455
+ # Every aggregate-owned port operation's own fully-qualified verb — only an
456
+ # aggregate's own ports are in scope; a policy trigger never names a
457
+ # chapter-level port directly.
942
458
  def port_verbs_of(bluebook)
943
459
  bluebook.aggregates.flat_map do |aggregate|
944
460
  aggregate.ports.flat_map do |port|
@@ -949,27 +465,13 @@ module Hecks
949
465
  end
950
466
  end
951
467
 
952
- # Every triggerable verb, as a triple — `verbs_of` (ordinary/entity
953
- # commands) plus `port_verbs_of` (port operations), each parsed
954
- # through `Naming.split_verb` so a caller never has to compare two
955
- # spellings of the same verb as strings (see `policy_findings`'s own
956
- # `unknown_trigger` check for why that comparison has to happen this
957
- # way, not as `include?` on a raw string).
958
- #
959
- # @param bluebook [Bluebook::Chapter] the assembled chapter to
960
- # enumerate
961
- # @return [Set<Array(String, String, String)>] every triggerable
962
- # verb, parsed as `Naming.split_verb` `[domain, aggregate,
963
- # command]` triples
468
+ # Every triggerable verb — ordinary/entity commands plus port operations —
469
+ # parsed through Naming.split_verb so callers never compare raw spellings.
964
470
  def triggerable_verbs(bluebook)
965
471
  (verbs_of(bluebook) + port_verbs_of(bluebook)).to_set { |verb| Naming.split_verb(verb) }
966
472
  end
967
473
 
968
474
  # Strips a domain qualifier off an event name.
969
- #
970
- # @param event [String, Symbol] an event name, qualified
971
- # (`"Domain::Event"`) or bare
972
- # @return [String] `event`'s own bare name, past the last `::`
973
475
  def bare(event) = event.to_s.split("::").last
974
476
  end
975
477
  end