hecks 1.2.0 → 1.4.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 (358) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +7 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +10 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +8 -8
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +31 -13
  7. data/lib/hecks/adapters/driven/heki/journal.rb +60 -2
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +5 -5
  9. data/lib/hecks/adapters/driven/heki.rb +13 -7
  10. data/lib/hecks/adapters/driven/identity_registry.rb +2 -2
  11. data/lib/hecks/adapters/driven/in_memory_ordering.rb +3 -3
  12. data/lib/hecks/adapters/driven/lambda/client.rb +34 -9
  13. data/lib/hecks/adapters/driven/lambda.rb +39 -33
  14. data/lib/hecks/adapters/driven/local_storage.rb +17 -10
  15. data/lib/hecks/adapters/driven/memory.rb +205 -9
  16. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +1 -1
  17. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  18. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  19. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  20. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  21. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  22. data/lib/hecks/adapters/driven/postgres_era.adapter +5 -0
  23. data/lib/hecks/adapters/driven/prism.rb +4 -4
  24. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  25. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  26. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  27. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  28. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  29. data/lib/hecks/adapters/driven.rb +4 -4
  30. data/lib/hecks/adapters/driving/github_webhook.rb +145 -0
  31. data/lib/hecks/behaviors/dsl.rb +2 -2
  32. data/lib/hecks/behaviors/expectations.rb +51 -23
  33. data/lib/hecks/behaviors/ir.rb +1 -1
  34. data/lib/hecks/behaviors/rspec.rb +1 -1
  35. data/lib/hecks/behaviors/runner.rb +2 -2
  36. data/lib/hecks/behaviors.rb +1 -1
  37. data/lib/hecks/bluebook/aggregate.rb +13 -13
  38. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
  39. data/lib/hecks/bluebook/assembly/build.rb +1 -1
  40. data/lib/hecks/bluebook/assembly/contract.rb +39 -16
  41. data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
  42. data/lib/hecks/bluebook/assembly/marks.rb +18 -18
  43. data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
  44. data/lib/hecks/bluebook/assembly.rb +9 -9
  45. data/lib/hecks/bluebook/attribute.rb +9 -9
  46. data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
  47. data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
  48. data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
  49. data/lib/hecks/bluebook/behaviour/command.rb +23 -23
  50. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -3
  51. data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
  52. data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
  53. data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
  54. data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
  55. data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
  56. data/lib/hecks/bluebook/behaviour/query.rb +1 -1
  57. data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
  58. data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
  59. data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
  60. data/lib/hecks/bluebook/capabilities.rb +27 -0
  61. data/lib/hecks/bluebook/chapter.rb +28 -9
  62. data/lib/hecks/bluebook/command.rb +12 -12
  63. data/lib/hecks/bluebook/domain_port.rb +9 -9
  64. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  65. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  66. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
  67. data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
  68. data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
  69. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
  70. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
  71. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  72. data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
  73. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  74. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  75. data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
  76. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  77. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
  78. data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
  79. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  80. data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
  81. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  82. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  83. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
  84. data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
  85. data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
  86. data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
  87. data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
  88. data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
  89. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  90. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  91. data/lib/hecks/bluebook/entity.rb +11 -11
  92. data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
  93. data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
  94. data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
  95. data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
  96. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
  97. data/lib/hecks/bluebook/expression/resolver.rb +60 -62
  98. data/lib/hecks/bluebook/hexagon.rb +1 -1
  99. data/lib/hecks/bluebook/lifecycle.rb +1 -1
  100. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
  101. data/lib/hecks/bluebook/meta_validator/judge.rb +122 -86
  102. data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
  103. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
  104. data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
  105. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
  106. data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
  107. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
  108. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
  109. data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
  110. data/lib/hecks/bluebook/meta_validator.rb +70 -70
  111. data/lib/hecks/bluebook/model_check.rb +301 -84
  112. data/lib/hecks/bluebook/pattern_subset.rb +9 -9
  113. data/lib/hecks/bluebook/policy.rb +15 -13
  114. data/lib/hecks/bluebook/process_manager.rb +14 -14
  115. data/lib/hecks/bluebook/project_register.rb +6 -6
  116. data/lib/hecks/bluebook/query.rb +4 -4
  117. data/lib/hecks/bluebook/read_model.rb +14 -14
  118. data/lib/hecks/bluebook/reference.rb +8 -8
  119. data/lib/hecks/bluebook/smoke_test.rb +19 -19
  120. data/lib/hecks/bluebook/synthesizer.rb +12 -12
  121. data/lib/hecks/bluebook/translation.rb +4 -4
  122. data/lib/hecks/bluebook/value_object.rb +6 -6
  123. data/lib/hecks/bluebook.rb +2 -2
  124. data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
  125. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
  126. data/lib/hecks/codemod.rb +36 -35
  127. data/lib/hecks/construct.rb +6 -6
  128. data/lib/hecks/corpus.rb +317 -0
  129. data/lib/hecks/deprecation.rb +95 -0
  130. data/lib/hecks/doc/reference.rb +19 -19
  131. data/lib/hecks/embryonaut_bluebook.rb +11 -11
  132. data/lib/hecks/facade/cli_door.rb +69 -10
  133. data/lib/hecks/facade/cli_runner.rb +105 -24
  134. data/lib/hecks/facade/command_request.rb +23 -0
  135. data/lib/hecks/facade/handle.rb +79 -32
  136. data/lib/hecks/facade/json_door.rb +106 -25
  137. data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
  138. data/lib/hecks/facade/surface/chapter.rb +26 -17
  139. data/lib/hecks/facade/surface.rb +16 -3
  140. data/lib/hecks/facade.rb +15 -4
  141. data/lib/hecks/forms/app.rb +46 -30
  142. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  143. data/lib/hecks/forms/field_renderer.rb +142 -6
  144. data/lib/hecks/forms/field_shape.rb +183 -20
  145. data/lib/hecks/forms/html.rb +51 -7
  146. data/lib/hecks/forms/index_renderer.rb +14 -2
  147. data/lib/hecks/forms/params.rb +120 -23
  148. data/lib/hecks/forms/port_argument.rb +2 -2
  149. data/lib/hecks/forms/query_form_renderer.rb +2 -2
  150. data/lib/hecks/forms/record_renderer.rb +2 -2
  151. data/lib/hecks/forms/record_table.rb +1 -1
  152. data/lib/hecks/forms/value_object_shape.rb +3 -3
  153. data/lib/hecks/forms.rb +24 -4
  154. data/lib/hecks/fqn.rb +1 -1
  155. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  156. data/lib/hecks/framework.rb +48 -17
  157. data/lib/hecks/freezer.rb +11 -11
  158. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
  159. data/lib/hecks/fuzzing/combination_miner.rb +119 -0
  160. data/lib/hecks/fuzzing/concurrent_dispatch.rb +361 -0
  161. data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
  162. data/lib/hecks/fuzzing/differential.rb +158 -0
  163. data/lib/hecks/fuzzing/domain_generator.rb +694 -0
  164. data/lib/hecks/fuzzing/era_boundary.rb +124 -0
  165. data/lib/hecks/fuzzing/form_census.rb +199 -0
  166. data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
  167. data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
  168. data/lib/hecks/fuzzing/isolated_boot.rb +226 -38
  169. data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
  170. data/lib/hecks/fuzzing/persistence_parity.rb +161 -0
  171. data/lib/hecks/fuzzing/properties/corrections.rb +100 -0
  172. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +321 -41
  173. data/lib/hecks/fuzzing/properties/guards.rb +129 -26
  174. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
  175. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +31 -11
  176. data/lib/hecks/fuzzing/properties/outbox.rb +142 -0
  177. data/lib/hecks/fuzzing/properties/querying.rb +17 -17
  178. data/lib/hecks/fuzzing/properties.rb +84 -39
  179. data/lib/hecks/fuzzing/qa_settings.rb +152 -0
  180. data/lib/hecks/fuzzing/replay.rb +224 -114
  181. data/lib/hecks/fuzzing/rotation_priority.rb +94 -0
  182. data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
  183. data/lib/hecks/fuzzing/self_consistency.rb +676 -0
  184. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +526 -0
  185. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +101 -30
  186. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +55 -8
  187. data/lib/hecks/fuzzing/sequence_generator/picker.rb +31 -11
  188. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +123 -32
  189. data/lib/hecks/fuzzing/sequence_generator.rb +152 -31
  190. data/lib/hecks/fuzzing/shrinker.rb +197 -0
  191. data/lib/hecks/fuzzing/structural_skips.rb +39 -0
  192. data/lib/hecks/fuzzing/sweep_depth.rb +53 -0
  193. data/lib/hecks/fuzzing/target_capabilities.rb +180 -0
  194. data/lib/hecks/fuzzing/value_generator.rb +66 -14
  195. data/lib/hecks/fuzzing.rb +11 -0
  196. data/lib/hecks/grammar/evolve.rb +10 -10
  197. data/lib/hecks/grammar.rb +7 -7
  198. data/lib/hecks/ir.rb +13 -13
  199. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  200. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  201. data/lib/hecks/language/bluebook/vocabulary.bluebook +365 -15
  202. data/lib/hecks/language/oidc.json +5 -0
  203. data/lib/hecks/literal.rb +9 -9
  204. data/lib/hecks/naming.rb +89 -21
  205. data/lib/hecks/ports/access_control.rb +58 -2
  206. data/lib/hecks/ports/agent/answers.rb +83 -6
  207. data/lib/hecks/ports/agent.rb +119 -35
  208. data/lib/hecks/ports/authentication.rb +44 -4
  209. data/lib/hecks/ports/authorization.rb +53 -11
  210. data/lib/hecks/ports/clock.rb +42 -23
  211. data/lib/hecks/ports/extraction.rb +16 -0
  212. data/lib/hecks/ports/identity_assignment.rb +24 -2
  213. data/lib/hecks/ports/identity_generation.rb +17 -3
  214. data/lib/hecks/ports/identity_resolution.rb +18 -1
  215. data/lib/hecks/ports/loading.rb +4 -0
  216. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  217. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  218. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  219. data/lib/hecks/ports/persistence/execution.rb +4 -0
  220. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  221. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  222. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  223. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  224. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  225. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  226. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  227. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  228. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  229. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  230. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  231. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +160 -58
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +44 -6
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +338 -83
  242. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  243. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  244. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  245. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
  246. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  247. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  248. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  249. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
  250. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
  251. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
  252. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  253. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  254. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  255. data/lib/hecks/ports/persistence/repository_factory.rb +29 -6
  256. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  257. data/lib/hecks/ports/persistence.rb +36 -1
  258. data/lib/hecks/ports/projection.rb +61 -7
  259. data/lib/hecks/ports/query/in_memory.rb +3 -3
  260. data/lib/hecks/ports/query/ordering.rb +6 -6
  261. data/lib/hecks/ports/query.rb +35 -0
  262. data/lib/hecks/projections/bootstrap_table.rb +112 -0
  263. data/lib/hecks/projections/diagrams.rb +75 -75
  264. data/lib/hecks/projections/glossary/html.rb +250 -0
  265. data/lib/hecks/projections/glossary/markdown.rb +105 -0
  266. data/lib/hecks/projections/glossary/mermaid.rb +110 -0
  267. data/lib/hecks/projections/glossary/page.css +271 -0
  268. data/lib/hecks/projections/glossary/page.js +72 -0
  269. data/lib/hecks/projections/glossary/sections.rb +17 -0
  270. data/lib/hecks/projections/glossary/sentences.rb +205 -0
  271. data/lib/hecks/projections/glossary.rb +214 -286
  272. data/lib/hecks/projections/ir.rb +1 -1
  273. data/lib/hecks/projections/model/deviations.rb +18 -17
  274. data/lib/hecks/projections/model.rb +25 -21
  275. data/lib/hecks/projections/oidc.rb +7 -7
  276. data/lib/hecks/projections/parser_table.rb +5 -5
  277. data/lib/hecks/projections/reference.rb +3 -3
  278. data/lib/hecks/projections/rust_vocabulary.rb +443 -0
  279. data/lib/hecks/projections/shape.rb +2 -2
  280. data/lib/hecks/projections/statements.rb +11 -11
  281. data/lib/hecks/projections/vocabulary.rb +9 -9
  282. data/lib/hecks/projections.rb +5 -3
  283. data/lib/hecks/projector/cli_projector.rb +29 -29
  284. data/lib/hecks/projector/docs_projector.rb +13 -13
  285. data/lib/hecks/projector/exporter.rb +42 -21
  286. data/lib/hecks/projector/ir_projector.rb +1 -1
  287. data/lib/hecks/projector/narrate_projector.rb +15 -22
  288. data/lib/hecks/projector/target.rb +13 -13
  289. data/lib/hecks/projector.rb +15 -15
  290. data/lib/hecks/query_ir.rb +47 -47
  291. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  292. data/lib/hecks/query_specification/common/comparison.rb +132 -24
  293. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  294. data/lib/hecks/query_specification/common/null_policy.rb +57 -13
  295. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  296. data/lib/hecks/query_specification/common/options.rb +25 -0
  297. data/lib/hecks/query_specification/field_path.rb +69 -15
  298. data/lib/hecks/query_specification/hop_path.rb +57 -20
  299. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  300. data/lib/hecks/rendering.rb +3 -3
  301. data/lib/hecks/router/namespace_installer.rb +3 -3
  302. data/lib/hecks/router.rb +1 -1
  303. data/lib/hecks/runtime/aggregate_lock.rb +11 -11
  304. data/lib/hecks/runtime/boot_gates.rb +3 -3
  305. data/lib/hecks/runtime/caller.rb +8 -8
  306. data/lib/hecks/runtime/capability_graph.rb +2 -2
  307. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
  308. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +64 -67
  309. data/lib/hecks/runtime/command_interpreter.rb +159 -102
  310. data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
  311. data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
  312. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  313. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  314. data/lib/hecks/runtime/command_rules/references.rb +94 -19
  315. data/lib/hecks/runtime/command_rules.rb +3 -0
  316. data/lib/hecks/runtime/dependency_planning.rb +11 -11
  317. data/lib/hecks/runtime/dispatcher.rb +283 -90
  318. data/lib/hecks/runtime/entity_element.rb +282 -46
  319. data/lib/hecks/runtime/entity_interpreter.rb +160 -69
  320. data/lib/hecks/runtime/errors.rb +19 -19
  321. data/lib/hecks/runtime/event.rb +6 -6
  322. data/lib/hecks/runtime/identity.rb +22 -22
  323. data/lib/hecks/runtime/instance.rb +39 -14
  324. data/lib/hecks/runtime/interpreting.rb +12 -12
  325. data/lib/hecks/runtime/invocation.rb +276 -0
  326. data/lib/hecks/runtime/loader.rb +14 -14
  327. data/lib/hecks/runtime/outbox.rb +23 -23
  328. data/lib/hecks/runtime/policy_interpreter.rb +54 -54
  329. data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
  330. data/lib/hecks/runtime/query_interpreter.rb +111 -56
  331. data/lib/hecks/runtime/reaction_invocation.rb +76 -9
  332. data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
  333. data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
  334. data/lib/hecks/runtime/reference_hop.rb +6 -6
  335. data/lib/hecks/runtime/refusal_wording.rb +92 -112
  336. data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
  337. data/lib/hecks/runtime/registry/verification.rb +36 -26
  338. data/lib/hecks/runtime/registry.rb +56 -27
  339. data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
  340. data/lib/hecks/runtime/routing.rb +10 -88
  341. data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
  342. data/lib/hecks/runtime/saga_interpreter.rb +93 -56
  343. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  344. data/lib/hecks/runtime/tenant_check.rb +9 -9
  345. data/lib/hecks/runtime/tenant_scope.rb +5 -5
  346. data/lib/hecks/runtime/value/admission.rb +75 -30
  347. data/lib/hecks/runtime/value/coercion.rb +379 -226
  348. data/lib/hecks/runtime/value/entity_list_coercion.rb +248 -0
  349. data/lib/hecks/runtime/value.rb +28 -23
  350. data/lib/hecks/runtime.rb +7 -7
  351. data/lib/hecks/storehouse.rb +64 -64
  352. data/lib/hecks/version.rb +3 -3
  353. data/lib/hecks/vocabulary.rb +207 -5
  354. data/lib/hecks.rb +13 -11
  355. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
  356. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
  357. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
  358. metadata +43 -2
@@ -1,17 +1,19 @@
1
+ require "hecks/vocabulary"
2
+
1
3
  module Hecks
2
4
  module Bluebook
3
5
  # Lightweight formal methods over the IR — the same family as TLA+/
4
6
  # Alloy/P: every lifecycle is a declared finite state machine and
5
- # every process manager a declared protocol, and BOTH are already
6
- # data, not code, so they can be MODEL-CHECKED rather than merely
7
+ # every process manager a declared protocol, and both are already
8
+ # data, not code, so they can be model-checked rather than merely
7
9
  # executed. Static analysis only — no bluebook boots twice, no
8
10
  # runtime is touched — over what meta_validator/judge.rb and the
9
11
  # builders' own `validate!` methods leave uncovered (an undeclared
10
12
  # transition target, a dispatch to nowhere, a compensation nothing
11
13
  # can ever reach).
12
14
  #
13
- # THE RARE PROPERTY THIS RESTS ON: the model IS the implementation.
14
- # A checker over TLA+ verifies a SPEC a human keeps in sync with
15
+ # **The rare property this rests on**: the model is the implementation.
16
+ # A checker over TLA+ verifies a spec a human keeps in sync with
15
17
  # code by hand ; this verifies the same IR the runtime dispatches
16
18
  # against, so there is no second copy to drift.
17
19
  module ModelCheck
@@ -19,8 +21,8 @@ module Hecks
19
21
  def to_s = "#{severity.to_s.upcase.ljust(7)} #{kind.to_s.ljust(20)} #{subject} — #{message}"
20
22
  end
21
23
 
22
- # A FINDING SHIPPED, NOT SILENCED — the coverage-gate idiom, empty
23
- # allowlists enforced BOTH directions (spec/model_check_spec.rb holds
24
+ # **A finding shipped, not silenced** — the coverage-gate idiom, empty
25
+ # allowlists enforced both directions (spec/model_check_spec.rb holds
24
26
  # this exact table: an error the checker reports and this does not
25
27
  # name is a regression, an entry the checker no longer reports is
26
28
  # stale and must be deleted). bin/model_check reads this same
@@ -29,42 +31,73 @@ module Hecks
29
31
  # "banking"/ExternalSettlement — found on the first real run:
30
32
  # ExternalSettlement declares `ends_on "ExternalTransferSent"`, and
31
33
  # ExternalTransfer.Send genuinely emits it — the event is real, and
32
- # the AGGREGATE reaches "sent" (its own, separate lifecycle) — but
33
- # the SAGA'S protocol has no `on "ExternalTransferSent"` handler, so
34
+ # the aggregate reaches "sent" (its own, separate lifecycle) — but
35
+ # the saga's protocol has no `on "ExternalTransferSent"` handler, so
34
36
  # its own `state "sent"` is unreachable through the chain the
35
37
  # checker walks, and the saga's own bookkeeping (saga_log, ends_on)
36
38
  # never closes it. Real domain activity is unaffected; the saga's
37
- # OWN tracking of it is not. Left named rather than redesigning a
39
+ # own tracking of it is not. Left named rather than redesigning a
38
40
  # corpus fixture that is not this checker's to redesign.
39
41
  # S7, ADR 0025 — the ExternalSettlement finding this used to
40
- # allowlist is GONE, not just quieted: its "sent" state was a
42
+ # allowlist is gone, not just quieted: its "sent" state was a
41
43
  # `state "x"` line never named by any handler's own from:/to:, a
42
- # pure declaration-drift artifact. States are DERIVED from the
44
+ # pure declaration-drift artifact. States are derived from the
43
45
  # transitions that name them now (ProcessManagerBuilder#derived_
44
46
  # states), so a state nothing ever transitions into or out of no
45
47
  # longer exists to be unreachable — the finding this allowlisted
46
48
  # cannot occur any more, by construction.
47
49
  #
48
- # "banking"/NotifyOnClosure, FlagKeyReturn — real, confirmed
49
- # findings, not bugs to fix. `across "Notifications"` names a
50
- # domain that does not exist anywhere in this repo no
51
- # Notifications bluebook, no hecksagon, nothing to `uses_framework`
52
- # or `subscribe` to. This is deliberate: `spec/runtime/policy_spec.
53
- # rb` (a test literally named "records a reaction it cannot
54
- # deliver rather than swallowing it") and `lib/hecks/runtime/
55
- # errors.rb`'s own `UnknownVerb` comment both treat "target domain
56
- # not loaded" as the EXPECTED outcome for it — Notifications is
57
- # used on purpose to exercise the undelivered-reaction runtime
58
- # path, not left half-built. There is no real `subscribe` line to
59
- # add (no event of Notifications' own to name) and no real domain
60
- # to point `uses_framework` at.
50
+ # "banking"/NotifyOnClosure, FlagKeyReturn — gone from here, moved
51
+ # to banking. `across "Notifications"` names a domain that does not
52
+ # exist anywhere in this repo, deliberately (it exercises the
53
+ # undelivered-reaction runtime path `spec/runtime/policy_spec.rb`,
54
+ # "records a reaction it cannot deliver rather than swallowing it").
55
+ # That expectation is now declared on the two policies themselves —
56
+ # `across "Notifications", expect_undelivered: true` and
57
+ # `expected_undelivered_findings` below holds it in both directions:
58
+ # the unknown-target and unacknowledged-relationship findings are
59
+ # expected, and a declaration whose target turns out reachable is a
60
+ # `stale_undelivered_expectation` error. A domain's own allowance
61
+ # lives in its own source, never in a core table keyed by its name.
62
+ #
63
+ # Pinned empty (spec/model_check_spec.rb), the way `bin/fuzz`'s
64
+ # `KNOWN_FUZZ_FINDINGS` is: a finding a domain means to keep belongs
65
+ # in that domain's own declaration.
61
66
  ALLOWED_FINDINGS = {
62
- "banking" => [
63
- [:unacknowledged_relationship, "NotifyOnClosure"],
64
- [:unknown_target_domain, "NotifyOnClosure"],
65
- [:unacknowledged_relationship, "FlagKeyReturn"],
66
- [:unknown_target_domain, "FlagKeyReturn"]
67
- ]
67
+ # QualityControl was the first domain in this corpus to trigger an
68
+ # `asks`/`tells` port operation from a `policy`, and used to carry
69
+ # two entries here for it — both gone now, not just quieted:
70
+ #
71
+ # `deaf_policy` (ClearOnPass, RefuseOnFail, RecordTheIssue,
72
+ # RecordTheRefusal) went first: `emitted_events` below now reads an
73
+ # outbound operation's `.answers`/`.refuses` the same way it already
74
+ # read a command's `.emits`, so `Clearance.SuitePassed`/`SuiteFailed`
75
+ # and `Ticket.IssueFiled`/`IssueFilingRefused` enter the known-emits
76
+ # set for real — the same fix `bin/qa_pr_check`'s own move to
77
+ # dispatching through the CI port (rather than `Clearance::Passed`/
78
+ # `Failed` directly) needed to make these two policies actually fire.
79
+ #
80
+ # `unknown_trigger` (FileWhenSubmitted, AskOnceMore) — BUG#23 — was
81
+ # never actually a `Naming`/`PolicyBuilder` defect, confirmed by
82
+ # tracing the real dispatch path rather than assuming the comment
83
+ # that used to sit here: `Naming.command_ref`'s bare-constant
84
+ # rewrite does leave `trigger Ticket::IssueTracker::File` (aggregate,
85
+ # port, operation) as "Ticket::IssueTracker.File", a leftover `::`
86
+ # past the aggregate — but `PolicyInterpreter#deliver` re-qualifies
87
+ # every trigger with this domain's own name before dispatch
88
+ # ("QualityControl::Ticket::IssueTracker.File"), and `Naming.
89
+ # split_verb` already folds that reintroduced `::` into the
90
+ # dot-joined tail correctly (fixed for `ReactionInvocation#
91
+ # resolve_target`, PR #520, predating this entry's own removal) —
92
+ # confirmed live: a real dispatch through `Ticket.Submit` fires
93
+ # `IssueFiled`/`TicketFiled` exactly as declared. The actual gap was
94
+ # entirely in this checker: `verbs_of` never enumerated a port
95
+ # operation as a triggerable verb at all, and `policy_findings`
96
+ # compared raw strings instead of `Naming.split_verb` triples the
97
+ # way `handler_findings`'s own `unknown_dispatch` check already does
98
+ # (BUG#6). Fixed with `port_verbs_of`/`triggerable_verbs`, scoped
99
+ # entirely to this file — no change to `Naming` or `PolicyBuilder`
100
+ # was needed or made.
68
101
  }.freeze
69
102
 
70
103
  module_function
@@ -72,16 +105,20 @@ module Hecks
72
105
  # `hecksagon:`/`known_domains:` — both optional, both `nil`-safe
73
106
  # (every existing caller with no sibling hecksagon, or checking one
74
107
  # domain in isolation, behaves exactly as before). `hecksagon` is
75
- # THIS bluebook's own sibling wiring file, if the caller loaded one
108
+ # this bluebook's own sibling wiring file, if the caller loaded one
76
109
  # (see `emitted_events`'s own comment on why a caller that didn't
77
110
  # simply finds none, correctly). `known_domains` is the caller's
78
- # OWN corpus-wide view — every bluebook/hecksagon name it has
111
+ # own corpus-wide view — every bluebook/hecksagon name it has
79
112
  # booted anywhere, across every domain it has looked at, not just
80
113
  # this one — used only to catch a typo'd `across`/`uses_framework`
81
114
  # target; see `cross_domain_policy_findings`'s own comment for why
82
115
  # this can only ever be a corpus-scoped heuristic, never a general
83
116
  # correctness guarantee.
84
- def call(bluebook, hecksagon: nil, known_domains: nil)
117
+ #
118
+ # `rust_target:`/`strict:` — both default false, both only change the
119
+ # severity of `rust_reserved_name` findings (see
120
+ # `rust_reserved_name_findings`); every other finding is unaffected.
121
+ def call(bluebook, hecksagon: nil, known_domains: nil, rust_target: false, strict: false)
85
122
  findings = []
86
123
  bluebook.aggregates.each do |aggregate|
87
124
  findings.concat(lifecycle_findings(aggregate, aggregate))
@@ -89,10 +126,68 @@ module Hecks
89
126
  end
90
127
  bluebook.process_managers.each { |process_manager| findings.concat(saga_findings(bluebook, process_manager)) }
91
128
  bluebook.policies.each { |policy| findings.concat(policy_findings(bluebook, policy, hecksagon, known_domains)) }
129
+ findings.concat(rust_reserved_name_findings(domain_name: bluebook.name,
130
+ aggregate_names: bluebook.aggregates.map(&:hecks_name),
131
+ rust_target: rust_target, strict: strict))
92
132
  findings
93
133
  end
94
134
 
95
- # ── lifecycles (aggregate AND entity — a piece may declare one too) ──
135
+ # ── Rust reserved names ───────────────────────────────────────────
136
+ #
137
+ # A name that becomes a bare Rust module identifier with no `r#`
138
+ # escape hatch: an aggregate (`pub mod <name.downcase>;` plus its
139
+ # `<name.downcase>.rs` file) and a domain (`pub mod <name>;` and a
140
+ # Cargo `[features]` key). Field names are not checked — both
141
+ # generators already raw-escape those (`rust_ident_field`).
142
+ #
143
+ # The words come from the `RustReservedWord`/`CargoReservedName`
144
+ # vocabularies, the same tables `rust/project/naming.rb` and
145
+ # hecks-codegen's generated `reserved_names.rs` read. Both Rust
146
+ # generators refuse through this check (`Projector.
147
+ # reserved_name_refusal`, and its hecks-codegen port in `naming.rs`).
148
+ #
149
+ # Severity: a domain that only ever runs in Ruby is fine with an
150
+ # aggregate named `Match`, so this warns by default. It is an error
151
+ # when the caller says the domain has a Rust target (`rust_target:` —
152
+ # `bin/model_check` reads it off the domain's Cargo feature, the
153
+ # generators always pass it) or asks for strictness (`strict:`,
154
+ # `bin/model_check --strict`).
155
+ #
156
+ # The module-name transform is `downcase`, the one both generators
157
+ # apply to an aggregate name and to an attached chapter's name.
158
+ def rust_reserved_name_findings(domain_name: nil, aggregate_names: [], rust_target: false, strict: false)
159
+ severity = rust_target || strict ? :error : :warning
160
+ keywords = Hecks::Vocabulary.fetch("RustReservedWord")
161
+
162
+ findings = aggregate_names.filter_map do |name|
163
+ module_name = rust_module_name(name)
164
+ next unless keywords.include?(module_name)
165
+
166
+ Finding.new(kind: :rust_reserved_name, severity: severity, subject: name.to_s,
167
+ message: "the aggregate's Rust module `#{module_name}` is a Rust keyword (RustReservedWord) — " \
168
+ "`pub mod #{module_name};` has no raw-identifier escape; rename the aggregate")
169
+ end
170
+ findings.concat(domain_reserved_name_findings(domain_name, keywords, severity)) if domain_name
171
+ findings
172
+ end
173
+
174
+ def domain_reserved_name_findings(domain_name, keywords, severity)
175
+ module_name = rust_module_name(domain_name)
176
+ table = if keywords.include?(module_name)
177
+ "a Rust keyword (RustReservedWord)"
178
+ elsif Hecks::Vocabulary.fetch("CargoReservedName").include?(module_name)
179
+ "a reserved Cargo.toml key (CargoReservedName)"
180
+ end
181
+ return [] unless table
182
+
183
+ [Finding.new(kind: :rust_reserved_name, severity: severity, subject: domain_name.to_s,
184
+ message: "the domain's Rust module and Cargo feature `#{module_name}` is #{table} — " \
185
+ "rename the domain")]
186
+ end
187
+
188
+ def rust_module_name(name) = name.to_s.downcase
189
+
190
+ # ── lifecycles (aggregate and entity — a piece may declare one too) ──
96
191
 
97
192
  def lifecycle_findings(aggregate, declaring)
98
193
  lifecycle = declaring.lifecycle
@@ -164,7 +259,7 @@ module Hecks
164
259
  ).uniq
165
260
  end
166
261
 
167
- # Least fixpoint from the default state: an UNCONSTRAINED
262
+ # Least fixpoint from the default state: an unconstrained
168
263
  # transition always fires, from wherever the machine is ; a
169
264
  # constrained one fires once any of its named sources is reached.
170
265
  def reachable_states(lifecycle)
@@ -188,8 +283,8 @@ module Hecks
188
283
  reached
189
284
  end
190
285
 
191
- # A state with no OUTGOING declared path at all is exempt from the
192
- # stuck-state WARNING for a different reason than "it fires an
286
+ # A state with no outgoing declared path at all is exempt from the
287
+ # stuck-state warning for a different reason than "it fires an
193
288
  # unconstrained transition" — the default state of a lifecycle
194
289
  # with only constrained transitions is legitimately allowed to sit
195
290
  # forever, since nothing about *entering* it via default implies
@@ -197,7 +292,7 @@ module Hecks
197
292
  # explicitly delivered somewhere.
198
293
  #
199
294
  # Scoped to `default` alone, and only when the lifecycle actually
200
- # declares real transitions elsewhere: an EMPTY lifecycle (no
295
+ # declares real transitions elsewhere: an empty lifecycle (no
201
296
  # transitions at all) doesn't get this exemption — that's not "a
202
297
  # machine whose entry point deliberately awaits external action,"
203
298
  # it's much more likely a lifecycle nobody finished wiring, and
@@ -215,7 +310,12 @@ module Hecks
215
310
 
216
311
  def saga_findings(bluebook, process_manager)
217
312
  emitted = emitted_events(bluebook)
218
- verbs = verbs_of(bluebook)
313
+ # (domain, aggregate, command) triples, not raw strings — see
314
+ # `handler_findings`'s own comment on the dispatch side for why:
315
+ # `Naming.split_verb` is what makes an entity verb's two legitimate
316
+ # spellings (`Naming.command_ref`'s own `::`-then-`.` rewrite vs.
317
+ # `verbs_of`'s own all-`.` one) compare equal.
318
+ verbs = verbs_of(bluebook).map { |verb| Naming.split_verb(verb) }
219
319
  reached = pm_reachable_states(process_manager, emitted)
220
320
 
221
321
  findings = []
@@ -246,11 +346,11 @@ module Hecks
246
346
  end
247
347
  end
248
348
 
249
- # ONE HANDLER'S OWN FINDINGS — deaf_handler, unknown_dispatch (one
349
+ # One handler's own findings — deaf_handler, unknown_dispatch (one
250
350
  # per dispatch), and unarmed_compensation (one per compensating
251
351
  # dispatch), pulled out of saga_findings' own handler loop; each
252
352
  # check reads only this handler plus the domain-wide emitted/verbs
253
- # sets saga_findings already resolved once, no state shared BETWEEN
353
+ # sets saga_findings already resolved once, no state shared between
254
354
  # handlers.
255
355
  def handler_findings(bluebook, process_manager, emitted, verbs, handler)
256
356
  findings = []
@@ -266,16 +366,38 @@ module Hecks
266
366
  end
267
367
 
268
368
  handler.dispatches.each do |dispatch|
269
- # SAME-DOMAIN, same as `SagaInterpreter#qualified` — a dispatch
270
- # naming no domain at all (the ordinary shape a bare command
271
- # constant now produces, S6) means THIS one, and is compared
272
- # against `verbs_of`'s own fully-qualified spelling qualified
273
- # the identical way, not left bare to miss it on a technicality.
274
- qualified = if dispatch.command_name.include?("::")
275
- dispatch.command_name
276
- else
277
- "#{bluebook.name}::#{dispatch.command_name}"
278
- end
369
+ # **Always this domain** — same fix, same reason, as `SagaInterpreter
370
+ # #qualified` (BUG#6). This used to guess: a dispatch whose own
371
+ # `command_name` still carried a leftover `::` after `Naming.
372
+ # command_ref`'s own rewrite was read as "already qualified" and
373
+ # left alone the exact same string-shape ambiguity that
374
+ # `SagaInterpreter#qualified`'s own comment explains at length
375
+ # (a same-domain entity command reference and a genuinely
376
+ # cross-domain one are textually indistinguishable after that
377
+ # rewrite). Confirmed against the entire corpus, same as that
378
+ # fix: no saga anywhere ever dispatches genuinely cross-domain,
379
+ # so this checker now qualifies exactly the way the runtime
380
+ # actually dispatches — unconditionally against `bluebook.name`
381
+ # — instead of maintaining its own, independently-wrong copy of
382
+ # the same guess.
383
+ #
384
+ # Compared as a triple, not a string — `Naming.command_ref`'s
385
+ # own rewrite of an entity reference (`Manifest::Slot::Fill`)
386
+ # collapses to "Manifest::Slot.Fill" (`::` between aggregate and
387
+ # entity, `.` before the command); `verbs_of`'s own entity
388
+ # spelling, below, joins aggregate/entity/command all with `.`
389
+ # instead (matching `fuzzing/sequence_generator/catalog.rb`'s own
390
+ # independent convention, its comment's own "the same spelling"
391
+ # claim). Both are legitimate, and `Naming.split_verb` already
392
+ # parses either to the identical (domain, aggregate, command)
393
+ # triple (its own comment: "past the already-resolved domain
394
+ # boundary, any leftover `::` is unambiguous... folding it into
395
+ # the dot-joined tail") — the same reading `ReactionInvocation.
396
+ # resolve_target` relies on at runtime. A bare string `include?`
397
+ # would falsely flag every entity dispatch as unknown_dispatch
398
+ # even once correctly domain-qualified, comparing two spellings
399
+ # of the same verb as though they were different ones.
400
+ qualified = Naming.split_verb("#{bluebook.name}::#{dispatch.command_name}")
279
401
  next if verbs.include?(qualified)
280
402
 
281
403
  findings << Finding.new(kind: :unknown_dispatch, severity: :error, subject: process_manager.name,
@@ -284,7 +406,7 @@ module Hecks
284
406
  "checker's scope, same as CommandRules#resolve_references")
285
407
  end
286
408
 
287
- # A `compensates` DECLARED WITH NOWHERE TO EVER FIRE — the exact
409
+ # A `compensates` declared with nowhere to ever fire — the exact
288
410
  # shape of the real bug this whole feature closes ("the
289
411
  # reversal was written and never armed"), caught at build/
290
412
  # model-check time instead of discovered in production. No
@@ -313,7 +435,7 @@ module Hecks
313
435
  end
314
436
 
315
437
  # A handler edge is only usable in the closure if it can actually
316
- # FIRE — REFUSED always can (it is a compensation trigger, not an
438
+ # fire — REFUSED always can (it is a compensation trigger, not an
317
439
  # event), and any other handler needs its event genuinely emitted.
318
440
  # Without this, a deaf handler's declared from_state -> to_state
319
441
  # pair reads as connected even though nothing can ever traverse
@@ -347,8 +469,8 @@ module Hecks
347
469
  emitted = emitted_events(bluebook)
348
470
  findings = []
349
471
 
350
- # `policy.event_name` (Naming.unqualified) — NOT `bare`, which only
351
- # strips a "::" domain qualifier. An AGGREGATE-scoped policy's
472
+ # `policy.event_name` (Naming.unqualified) — not `bare`, which only
473
+ # strips a "::" domain qualifier. An aggregate-scoped policy's
352
474
  # `on_event` carries a "." aggregate qualifier instead (PolicyBuilder
353
475
  # stores whatever was typed, verbatim — see `on "Account.
354
476
  # AccountFrozen"`), and `bare` left it untouched, silently comparing
@@ -366,9 +488,27 @@ module Hecks
366
488
  # `trigger` is spelled "Aggregate.Command" (or "Entity.Command" one
367
489
  # level down), completed to an FQN by PolicyInterpreter#deliver as
368
490
  # "#{domain}::#{trigger_command}" — the same join `verbs_of` builds
369
- # independently, so the two spellings have to be compared as FQNs,
370
- # never as bare command names.
371
- unless verbs_of(bluebook).include?("#{bluebook.name}::#{policy.trigger_command}")
491
+ # independently, so the two spellings have to be compared as FQNs.
492
+ #
493
+ # Compared as a triple, not a string — `handler_findings`'s own
494
+ # `unknown_dispatch` check (BUG#6) already applies this fix for a
495
+ # saga's dispatch; a policy's `trigger` needed the identical one. A
496
+ # policy triggering an `asks`/`tells` port operation (`Aggregate::
497
+ # Port::Operation`, three colon-joined segments — `Naming.command_ref`'s
498
+ # bare-constant rewrite turns this into "Aggregate::Port.Operation",
499
+ # a leftover `::` past the aggregate) is a real, working dispatch —
500
+ # `PolicyInterpreter#deliver` qualifies it with this domain's own
501
+ # name before `Naming.split_verb` ever sees it, and `split_verb`
502
+ # already folds that leftover `::` into the dot-joined tail
503
+ # correctly (fixed for `ReactionInvocation#resolve_target`, PR
504
+ # #520) — but this check compared raw strings against `verbs_of`,
505
+ # which never enumerated port operations at all, so it reported
506
+ # every port-operation trigger as unknown regardless. `triggerable_
507
+ # verbs` now includes both, and both sides are parsed through
508
+ # `Naming.split_verb` before comparing, the same reading
509
+ # `resolve_target` relies on at runtime.
510
+ qualified = Naming.split_verb("#{bluebook.name}::#{policy.trigger_command}")
511
+ unless qualified && triggerable_verbs(bluebook).include?(qualified)
372
512
  findings << Finding.new(kind: :unknown_trigger, severity: :error, subject: policy.name,
373
513
  message: "trigger #{policy.trigger_command.inspect} resolves to no command " \
374
514
  "this domain declares")
@@ -379,17 +519,17 @@ module Hecks
379
519
 
380
520
  # ── cross-domain policies (Context Mapping) ───────────────────────
381
521
  #
382
- # `uses_framework "X"` already IS a Shared Kernel relationship — it
383
- # merges X's own bluebook into THIS registry, no boundary. A cross-
384
- # domain `policy ... across: "X"` already IS a Customer/Supplier
522
+ # `uses_framework "X"` already is a Shared Kernel relationship — it
523
+ # merges X's own bluebook into this registry, no boundary. A cross-
524
+ # domain `policy ... across: "X"` already is a Customer/Supplier
385
525
  # relationship — it dispatches into X over real cross-Lambda RPC in
386
526
  # the Rust host (`rust/host/src/lambda_client.rs`). Neither is a new
387
- # word; this makes the CHOICE between them checked instead of a
527
+ # word; this makes the choice between them checked instead of a
388
528
  # prose comment nobody enforces (`examples/banking/bluebook/
389
529
  # banking.hecksagon`'s own hand-written note explaining why
390
530
  # Compliance is reached via `across`, never `uses_framework`).
391
531
  #
392
- # NO NEW KEYWORD ANYWHERE — ADR 0025 principle 1 ("one idea, one
532
+ # No new keyword anywhere — ADR 0025 principle 1 ("one idea, one
393
533
  # spelling") refuses a `relationship:`/`as:` argument that would
394
534
  # just restate, as a string, the fact the chosen keyword (
395
535
  # `uses_framework` vs `across`) already states completely. The
@@ -397,14 +537,15 @@ module Hecks
397
537
  # the finding's own name and this comment, and in prose docs — not
398
538
  # in the grammar.
399
539
  def cross_domain_policy_findings(policy, hecksagon, known_domains)
540
+ return expected_undelivered_findings(policy, hecksagon, known_domains) if policy.expect_undelivered
400
541
  return [] unless hecksagon # no sibling hecksagon loaded — nothing to check a relationship against.
401
542
 
402
543
  target = policy.target_domain
403
544
  findings = []
404
545
 
405
546
  if hecksagon.framework_members.include?(target)
406
- # SHARED KERNEL AND CUSTOMER/SUPPLIER ARE MUTUALLY EXCLUSIVE
407
- # CLAIMS about the SAME target — `uses_framework` means "X is
547
+ # Shared kernel and customer/supplier are mutually exclusive
548
+ # claims about the same target — `uses_framework` means "X is
408
549
  # loaded in-process, right here"; `across` means "X is a
409
550
  # separate deployment, reached only by RPC." Declaring both is
410
551
  # either a pointless RPC to a domain already local, or a
@@ -416,7 +557,7 @@ module Hecks
416
557
  "relationship declarations contradict each other for the same " \
417
558
  "target domain")
418
559
  elsif hecksagon.subscriptions.none? { |subscribed| Naming.qualifier(subscribed) == target }
419
- # THIS IS WHAT FINALLY GIVES `subscribe` REAL TEETH — checked
560
+ # This is what finally gives `subscribe` real teeth — checked
420
561
  # here, at model-check time, still never routed at runtime
421
562
  # (nothing dispatches off a `subscribe` line; see hecksagon.md's
422
563
  # own "checked, not routed" section). ADR 0025 names `subscribe`
@@ -429,50 +570,97 @@ module Hecks
429
570
  "attach it in-process instead")
430
571
  end
431
572
 
432
- # TYPO DETECTION, DELIBERATELY WEAKER — `known_domains` can only
433
- # ever be a MONOREPO-SCOPED heuristic: a real external hecks
573
+ # **Typo detection, deliberately weaker** — `known_domains` can only
574
+ # ever be a monorepo-scoped heuristic: a real external hecks
434
575
  # consumer's own domain (this repo's own embryonaut/lifeadelics-
435
576
  # shaped case) lives in a genuinely separate repository this
436
577
  # corpus scan can never see, so a target this check cannot find
437
- # is "unknown to THIS corpus," never proof of a typo. Two real,
438
- # legitimate reasons a target is unresolvable genuinely
439
- # undefined by design (the corpus's own "Notifications," used
578
+ # is "unknown to THIS corpus," never proof of a typo. A target
579
+ # undefined by design (the corpus's own "Notifications", used
440
580
  # deliberately to exercise the undelivered-reaction runtime path)
441
- # and real-but-external (a separate repository)both go in
442
- # `ALLOWED_FINDINGS`, the same judged-exception mechanism this
443
- # file already uses for ExternalSettlement, rather than a new
444
- # keyword invented to declare "this one's fine."
581
+ # declares so on its own policy `expect_undelivered: true`,
582
+ # checked by `expected_undelivered_findings` rather than being
583
+ # named in a core allowlist.
445
584
  if known_domains && !known_domains.include?(target)
446
585
  findings << Finding.new(kind: :unknown_target_domain, severity: :error, subject: policy.name,
447
586
  message: "across #{target.inspect} names a domain nowhere in the corpus " \
448
- "this check has booted — a typo, or a real domain intentionally " \
449
- "outside this corpus (undefined by design, or living in a " \
450
- "separate repository) belongs in ALLOWED_FINDINGS, named and " \
451
- "explained, not silently assumed correct")
587
+ "this check has booted — a typo, or a target intentionally never " \
588
+ "reached, which the policy itself declares with " \
589
+ "across #{target.inspect}, expect_undelivered: true")
452
590
  end
453
591
 
454
592
  findings
455
593
  end
456
594
 
595
+ # A declared undelivered target, held to its declaration. The two
596
+ # findings an unreachable `across` target raises (unknown target,
597
+ # unacknowledged relationship) are what the policy declared it
598
+ # expects, so they are not raised. What is raised is the declaration
599
+ # going stale: the target is a domain this corpus actually booted,
600
+ # or the sibling hecksagon attaches or subscribes to it — either way
601
+ # the reaction can be delivered, and the declaration is now a lie.
602
+ # `known_domains` is nil for a single-target run, which can then only
603
+ # check the hecksagon half.
604
+ def expected_undelivered_findings(policy, hecksagon, known_domains)
605
+ target = policy.target_domain
606
+ reached = []
607
+ reached << "#{target} is a domain this corpus boots" if known_domains&.include?(target)
608
+ if hecksagon
609
+ reached << "this hecksagon uses_framework #{target.inspect}" if hecksagon.framework_members.include?(target)
610
+ if hecksagon.subscriptions.any? { |subscribed| Naming.qualifier(subscribed) == target }
611
+ reached << "this hecksagon subscribes to #{target}"
612
+ end
613
+ end
614
+ return [] if reached.empty?
615
+
616
+ [Finding.new(kind: :stale_undelivered_expectation, severity: :error, subject: policy.name,
617
+ message: "across #{target.inspect}, expect_undelivered: true — but #{reached.join(' and ')}, " \
618
+ "so the reaction can be delivered after all; drop expect_undelivered: or remove " \
619
+ "what reaches #{target}")]
620
+ end
621
+
457
622
  # ── shared enumeration ────────────────────────────────────────────
458
623
 
459
- # A PORT OPERATION EMITS TOO — the primary/driving port an adapter
624
+ # A port operation emits too — the primary/driving port an adapter
460
625
  # outside the bluebook calls through (see hecksagon_builder.rb) is a
461
626
  # second, real source of events, alongside a command's own `emits`.
462
- # Ports attach to the aggregate/bluebook from the SIBLING `.hecksagon`
627
+ # Ports attach to the aggregate/bluebook from the sibling `.hecksagon`
463
628
  # file, not this one — a caller that boots only the `.bluebook` (as
464
629
  # the fixtures under spec/fixtures/model_check/ do, having no
465
630
  # hecksagon at all) simply finds none, which is correct : nothing
466
631
  # can be deaf to an event that isn't even wired up yet.
632
+ #
633
+ # An outbound operation (`asks`) emits through a different door — it
634
+ # declares no `.emits` at all (`PortOperationBuilder#refuse_wrong_
635
+ # words!` refuses one that tries), naming its two real endings
636
+ # `.answers`/`.refuses` instead (`PortOperation#initialize`). Reading
637
+ # only `.emits` left every `asks`'s own two events invisible to this
638
+ # method — real, live events a policy genuinely reacts to
639
+ # (`Clearance.SuitePassed`/`SuiteFailed`, `Ticket.IssueFiled`/
640
+ # `IssueFilingRefused`), reported as `deaf_policy` findings until this
641
+ # read both. `.compact` because an inbound operation's `.answers`/
642
+ # `.refuses` are always nil (there is no channel back to tell), which
643
+ # would otherwise seed every emitted-events set with a stray nil.
467
644
  def emitted_events(bluebook)
468
645
  aggregate_emits = bluebook.aggregates.flat_map do |aggregate|
469
646
  aggregate.commands.map(&:emits) +
470
647
  aggregate.entities.flat_map { |entity| entity.commands.map(&:emits) } +
471
- aggregate.ports.flat_map { |port| port.operations.map(&:emits) }
648
+ port_operation_events(aggregate.ports)
472
649
  end
473
- chapter_emits = bluebook.ports.flat_map { |port| port.operations.map(&:emits) }
650
+ chapter_emits = port_operation_events(bluebook.ports)
651
+
652
+ (aggregate_emits + chapter_emits).flatten.compact.uniq
653
+ end
474
654
 
475
- (aggregate_emits + chapter_emits).flatten.uniq
655
+ # One operation, either of its own sources of events — an inbound
656
+ # `tells` names its own via `.emits`; an outbound `asks` has none
657
+ # (`PortOperationBuilder#refuse_wrong_words!` refuses one that
658
+ # tries) and names its two real endings `.answers`/`.refuses`
659
+ # instead. Pulled out of `emitted_events` above purely to keep that
660
+ # method's own branching low enough to read at a glance — every
661
+ # port, aggregate-owned or chapter-level, asks this the same way.
662
+ def port_operation_events(ports)
663
+ ports.flat_map { |port| port.operations.flat_map { |op| [*op.emits, op.answers, op.refuses] } }
476
664
  end
477
665
 
478
666
  # Fully-qualified, the same spelling DispatchSpec#command_name
@@ -490,6 +678,35 @@ module Hecks
490
678
  end
491
679
  end
492
680
 
681
+ # An aggregate-owned port operation is a triggerable verb too —
682
+ # `ReactionInvocation#resolve_target`'s own port-operation branch
683
+ # resolves one by the exact same two-segment tail shape ("Aggregate::
684
+ # Port.Operation", the aggregate then the port then the operation,
685
+ # dot-joined past the domain) an entity command uses, checked first,
686
+ # same order `Dispatcher#dispatch` already resolves a live verb in.
687
+ # Only an aggregate's own ports (`aggregate.ports`) are in scope here
688
+ # — a policy's `trigger` always names one aggregate, never a chapter-
689
+ # level port with no owner to address through.
690
+ def port_verbs_of(bluebook)
691
+ bluebook.aggregates.flat_map do |aggregate|
692
+ aggregate.ports.flat_map do |port|
693
+ port.operations.map do |operation|
694
+ "#{bluebook.name}::#{aggregate.hecks_name}.#{port.name}.#{operation.hecks_name}"
695
+ end
696
+ end
697
+ end
698
+ end
699
+
700
+ # Every triggerable verb, as a triple — `verbs_of` (ordinary/entity
701
+ # commands) plus `port_verbs_of` (port operations), each parsed
702
+ # through `Naming.split_verb` so a caller never has to compare two
703
+ # spellings of the same verb as strings (see `policy_findings`'s own
704
+ # `unknown_trigger` check for why that comparison has to happen this
705
+ # way, not as `include?` on a raw string).
706
+ def triggerable_verbs(bluebook)
707
+ (verbs_of(bluebook) + port_verbs_of(bluebook)).to_set { |verb| Naming.split_verb(verb) }
708
+ end
709
+
493
710
  def bare(event) = event.to_s.split("::").last
494
711
  end
495
712
  end