hecks 1.3.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 (355) 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 +5 -4
  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/prism.rb +4 -4
  23. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  24. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  25. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  26. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  27. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  28. data/lib/hecks/adapters/driven.rb +4 -4
  29. data/lib/hecks/adapters/driving/github_webhook.rb +18 -18
  30. data/lib/hecks/behaviors/dsl.rb +2 -2
  31. data/lib/hecks/behaviors/expectations.rb +23 -23
  32. data/lib/hecks/behaviors/ir.rb +1 -1
  33. data/lib/hecks/behaviors/rspec.rb +1 -1
  34. data/lib/hecks/behaviors/runner.rb +2 -2
  35. data/lib/hecks/behaviors.rb +1 -1
  36. data/lib/hecks/bluebook/aggregate.rb +13 -13
  37. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +10 -10
  38. data/lib/hecks/bluebook/assembly/build.rb +1 -1
  39. data/lib/hecks/bluebook/assembly/contract.rb +39 -16
  40. data/lib/hecks/bluebook/assembly/contracts.rb +40 -38
  41. data/lib/hecks/bluebook/assembly/marks.rb +18 -18
  42. data/lib/hecks/bluebook/assembly/specializer.rb +19 -21
  43. data/lib/hecks/bluebook/assembly.rb +9 -9
  44. data/lib/hecks/bluebook/attribute.rb +9 -9
  45. data/lib/hecks/bluebook/behaviour/aggregate.rb +11 -11
  46. data/lib/hecks/bluebook/behaviour/attribute.rb +5 -5
  47. data/lib/hecks/bluebook/behaviour/chapter.rb +23 -5
  48. data/lib/hecks/bluebook/behaviour/command.rb +23 -23
  49. data/lib/hecks/bluebook/behaviour/domain_port.rb +7 -7
  50. data/lib/hecks/bluebook/behaviour/entity.rb +8 -8
  51. data/lib/hecks/bluebook/behaviour/hexagon.rb +4 -4
  52. data/lib/hecks/bluebook/behaviour/lifecycle.rb +5 -5
  53. data/lib/hecks/bluebook/behaviour/policy.rb +12 -12
  54. data/lib/hecks/bluebook/behaviour/process_manager.rb +7 -7
  55. data/lib/hecks/bluebook/behaviour/query.rb +1 -1
  56. data/lib/hecks/bluebook/behaviour/read_model.rb +8 -8
  57. data/lib/hecks/bluebook/behaviour/traits.rb +12 -12
  58. data/lib/hecks/bluebook/behaviour/value_object.rb +6 -6
  59. data/lib/hecks/bluebook/capabilities.rb +27 -0
  60. data/lib/hecks/bluebook/chapter.rb +28 -9
  61. data/lib/hecks/bluebook/command.rb +12 -12
  62. data/lib/hecks/bluebook/domain_port.rb +9 -9
  63. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  64. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  65. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +96 -96
  66. data/lib/hecks/bluebook/dsl/attribute_collector.rb +41 -41
  67. data/lib/hecks/bluebook/dsl/binding_proxy.rb +22 -2
  68. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +111 -74
  69. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +48 -30
  70. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  71. data/lib/hecks/bluebook/dsl/command_builder.rb +103 -103
  72. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  73. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  74. data/lib/hecks/bluebook/dsl/entity_builder.rb +56 -56
  75. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  76. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +89 -30
  77. data/lib/hecks/bluebook/dsl/identity_declaration.rb +17 -17
  78. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  79. data/lib/hecks/bluebook/dsl/policy_builder.rb +30 -21
  80. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  81. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  82. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +35 -35
  83. data/lib/hecks/bluebook/dsl/query_builder.rb +5 -5
  84. data/lib/hecks/bluebook/dsl/read_model_builder.rb +34 -34
  85. data/lib/hecks/bluebook/dsl/rule_reference.rb +41 -39
  86. data/lib/hecks/bluebook/dsl/translation_builder.rb +9 -9
  87. data/lib/hecks/bluebook/dsl/value_object_builder.rb +16 -16
  88. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  89. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  90. data/lib/hecks/bluebook/entity.rb +11 -11
  91. data/lib/hecks/bluebook/expression/ast_json.rb +20 -20
  92. data/lib/hecks/bluebook/expression/ast_reader.rb +3 -3
  93. data/lib/hecks/bluebook/expression/canonical_form.rb +9 -9
  94. data/lib/hecks/bluebook/expression/evaluator.rb +18 -18
  95. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +18 -18
  96. data/lib/hecks/bluebook/expression/resolver.rb +60 -62
  97. data/lib/hecks/bluebook/hexagon.rb +1 -1
  98. data/lib/hecks/bluebook/lifecycle.rb +1 -1
  99. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -1
  100. data/lib/hecks/bluebook/meta_validator/judge.rb +101 -87
  101. data/lib/hecks/bluebook/meta_validator/plan.rb +39 -39
  102. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -2
  103. data/lib/hecks/bluebook/meta_validator/readings.rb +43 -43
  104. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +44 -37
  105. data/lib/hecks/bluebook/meta_validator/shapes.rb +25 -21
  106. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +145 -31
  107. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +6 -6
  108. data/lib/hecks/bluebook/meta_validator/world_judge.rb +5 -5
  109. data/lib/hecks/bluebook/meta_validator.rb +70 -70
  110. data/lib/hecks/bluebook/model_check.rb +170 -84
  111. data/lib/hecks/bluebook/pattern_subset.rb +9 -9
  112. data/lib/hecks/bluebook/policy.rb +15 -13
  113. data/lib/hecks/bluebook/process_manager.rb +14 -14
  114. data/lib/hecks/bluebook/project_register.rb +6 -6
  115. data/lib/hecks/bluebook/query.rb +4 -4
  116. data/lib/hecks/bluebook/read_model.rb +14 -14
  117. data/lib/hecks/bluebook/reference.rb +8 -8
  118. data/lib/hecks/bluebook/smoke_test.rb +19 -19
  119. data/lib/hecks/bluebook/synthesizer.rb +12 -12
  120. data/lib/hecks/bluebook/translation.rb +4 -4
  121. data/lib/hecks/bluebook/value_object.rb +6 -6
  122. data/lib/hecks/bluebook.rb +2 -2
  123. data/lib/hecks/codemod/legacy_dispatch_args.rb +299 -0
  124. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +186 -0
  125. data/lib/hecks/codemod.rb +36 -35
  126. data/lib/hecks/construct.rb +6 -6
  127. data/lib/hecks/corpus.rb +317 -0
  128. data/lib/hecks/deprecation.rb +95 -0
  129. data/lib/hecks/doc/reference.rb +19 -19
  130. data/lib/hecks/embryonaut_bluebook.rb +11 -11
  131. data/lib/hecks/facade/cli_door.rb +69 -10
  132. data/lib/hecks/facade/cli_runner.rb +105 -24
  133. data/lib/hecks/facade/command_request.rb +23 -0
  134. data/lib/hecks/facade/handle.rb +79 -32
  135. data/lib/hecks/facade/json_door.rb +106 -25
  136. data/lib/hecks/facade/surface/aggregate_door.rb +42 -27
  137. data/lib/hecks/facade/surface/chapter.rb +26 -17
  138. data/lib/hecks/facade/surface.rb +16 -3
  139. data/lib/hecks/facade.rb +15 -4
  140. data/lib/hecks/forms/app.rb +46 -30
  141. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  142. data/lib/hecks/forms/field_renderer.rb +142 -6
  143. data/lib/hecks/forms/field_shape.rb +180 -19
  144. data/lib/hecks/forms/html.rb +51 -7
  145. data/lib/hecks/forms/index_renderer.rb +14 -2
  146. data/lib/hecks/forms/params.rb +120 -23
  147. data/lib/hecks/forms/port_argument.rb +2 -2
  148. data/lib/hecks/forms/query_form_renderer.rb +2 -2
  149. data/lib/hecks/forms/record_renderer.rb +2 -2
  150. data/lib/hecks/forms/record_table.rb +1 -1
  151. data/lib/hecks/forms/value_object_shape.rb +3 -3
  152. data/lib/hecks/forms.rb +24 -4
  153. data/lib/hecks/fqn.rb +1 -1
  154. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  155. data/lib/hecks/framework.rb +48 -17
  156. data/lib/hecks/freezer.rb +11 -11
  157. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +90 -90
  158. data/lib/hecks/fuzzing/combination_miner.rb +119 -0
  159. data/lib/hecks/fuzzing/concurrent_dispatch.rb +140 -45
  160. data/lib/hecks/fuzzing/coverage_campaign.rb +118 -0
  161. data/lib/hecks/fuzzing/differential.rb +158 -0
  162. data/lib/hecks/fuzzing/domain_generator.rb +694 -0
  163. data/lib/hecks/fuzzing/era_boundary.rb +37 -18
  164. data/lib/hecks/fuzzing/form_census.rb +35 -20
  165. data/lib/hecks/fuzzing/generated_domain_check.rb +95 -0
  166. data/lib/hecks/fuzzing/invalid_value_generator.rb +6 -6
  167. data/lib/hecks/fuzzing/isolated_boot.rb +62 -62
  168. data/lib/hecks/fuzzing/nondeterministic.rb +67 -0
  169. data/lib/hecks/fuzzing/persistence_parity.rb +19 -21
  170. data/lib/hecks/fuzzing/properties/corrections.rb +9 -9
  171. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +154 -49
  172. data/lib/hecks/fuzzing/properties/guards.rb +42 -42
  173. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +32 -32
  174. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +22 -35
  175. data/lib/hecks/fuzzing/properties/outbox.rb +24 -24
  176. data/lib/hecks/fuzzing/properties/querying.rb +17 -17
  177. data/lib/hecks/fuzzing/properties.rb +64 -40
  178. data/lib/hecks/fuzzing/qa_settings.rb +152 -0
  179. data/lib/hecks/fuzzing/replay.rb +114 -114
  180. data/lib/hecks/fuzzing/rotation_priority.rb +10 -10
  181. data/lib/hecks/fuzzing/rust_gap_manifest.rb +113 -0
  182. data/lib/hecks/fuzzing/self_consistency.rb +141 -112
  183. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +35 -35
  184. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
  185. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
  186. data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
  187. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +58 -31
  188. data/lib/hecks/fuzzing/sequence_generator.rb +111 -23
  189. data/lib/hecks/fuzzing/shrinker.rb +197 -0
  190. data/lib/hecks/fuzzing/structural_skips.rb +23 -130
  191. data/lib/hecks/fuzzing/sweep_depth.rb +4 -4
  192. data/lib/hecks/fuzzing/target_capabilities.rb +49 -18
  193. data/lib/hecks/fuzzing/value_generator.rb +14 -14
  194. data/lib/hecks/fuzzing.rb +5 -0
  195. data/lib/hecks/grammar/evolve.rb +10 -10
  196. data/lib/hecks/grammar.rb +7 -7
  197. data/lib/hecks/ir.rb +13 -13
  198. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  199. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  200. data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
  201. data/lib/hecks/language/oidc.json +5 -0
  202. data/lib/hecks/literal.rb +9 -9
  203. data/lib/hecks/naming.rb +25 -25
  204. data/lib/hecks/ports/access_control.rb +58 -2
  205. data/lib/hecks/ports/agent/answers.rb +83 -6
  206. data/lib/hecks/ports/agent.rb +119 -35
  207. data/lib/hecks/ports/authentication.rb +44 -4
  208. data/lib/hecks/ports/authorization.rb +53 -11
  209. data/lib/hecks/ports/clock.rb +42 -23
  210. data/lib/hecks/ports/extraction.rb +16 -0
  211. data/lib/hecks/ports/identity_assignment.rb +24 -2
  212. data/lib/hecks/ports/identity_generation.rb +17 -3
  213. data/lib/hecks/ports/identity_resolution.rb +18 -1
  214. data/lib/hecks/ports/loading.rb +4 -0
  215. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  216. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  217. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  218. data/lib/hecks/ports/persistence/execution.rb +4 -0
  219. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  220. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  221. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  222. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  223. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  224. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  225. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  226. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  227. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  228. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  229. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  230. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
  231. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  232. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +28 -5
  233. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +57 -4
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
  241. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  242. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  243. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  244. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +42 -8
  245. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  246. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  247. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  248. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -19
  249. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +5 -5
  250. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +1 -1
  251. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  252. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  253. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  254. data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
  255. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  256. data/lib/hecks/ports/persistence.rb +36 -1
  257. data/lib/hecks/ports/projection.rb +61 -7
  258. data/lib/hecks/ports/query/in_memory.rb +3 -3
  259. data/lib/hecks/ports/query/ordering.rb +6 -6
  260. data/lib/hecks/ports/query.rb +35 -0
  261. data/lib/hecks/projections/bootstrap_table.rb +112 -0
  262. data/lib/hecks/projections/diagrams.rb +75 -75
  263. data/lib/hecks/projections/glossary/html.rb +3 -3
  264. data/lib/hecks/projections/glossary/markdown.rb +3 -3
  265. data/lib/hecks/projections/glossary/mermaid.rb +1 -1
  266. data/lib/hecks/projections/glossary/sections.rb +1 -1
  267. data/lib/hecks/projections/glossary/sentences.rb +5 -5
  268. data/lib/hecks/projections/glossary.rb +15 -15
  269. data/lib/hecks/projections/ir.rb +1 -1
  270. data/lib/hecks/projections/model/deviations.rb +18 -17
  271. data/lib/hecks/projections/model.rb +25 -21
  272. data/lib/hecks/projections/oidc.rb +7 -7
  273. data/lib/hecks/projections/parser_table.rb +5 -5
  274. data/lib/hecks/projections/reference.rb +3 -3
  275. data/lib/hecks/projections/rust_vocabulary.rb +443 -0
  276. data/lib/hecks/projections/shape.rb +2 -2
  277. data/lib/hecks/projections/statements.rb +11 -11
  278. data/lib/hecks/projections/vocabulary.rb +9 -9
  279. data/lib/hecks/projections.rb +5 -3
  280. data/lib/hecks/projector/cli_projector.rb +29 -29
  281. data/lib/hecks/projector/docs_projector.rb +13 -13
  282. data/lib/hecks/projector/exporter.rb +42 -21
  283. data/lib/hecks/projector/ir_projector.rb +1 -1
  284. data/lib/hecks/projector/narrate_projector.rb +11 -11
  285. data/lib/hecks/projector/target.rb +13 -13
  286. data/lib/hecks/projector.rb +15 -15
  287. data/lib/hecks/query_ir.rb +47 -47
  288. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  289. data/lib/hecks/query_specification/common/comparison.rb +109 -27
  290. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  291. data/lib/hecks/query_specification/common/null_policy.rb +57 -13
  292. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  293. data/lib/hecks/query_specification/common/options.rb +25 -0
  294. data/lib/hecks/query_specification/field_path.rb +69 -15
  295. data/lib/hecks/query_specification/hop_path.rb +57 -20
  296. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  297. data/lib/hecks/rendering.rb +3 -3
  298. data/lib/hecks/router/namespace_installer.rb +3 -3
  299. data/lib/hecks/router.rb +1 -1
  300. data/lib/hecks/runtime/aggregate_lock.rb +11 -11
  301. data/lib/hecks/runtime/boot_gates.rb +3 -3
  302. data/lib/hecks/runtime/caller.rb +8 -8
  303. data/lib/hecks/runtime/capability_graph.rb +2 -2
  304. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +22 -25
  305. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +42 -63
  306. data/lib/hecks/runtime/command_interpreter.rb +159 -102
  307. data/lib/hecks/runtime/command_rules/admissibility.rb +69 -69
  308. data/lib/hecks/runtime/command_rules/arithmetic.rb +137 -91
  309. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  310. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  311. data/lib/hecks/runtime/command_rules/references.rb +35 -35
  312. data/lib/hecks/runtime/command_rules.rb +3 -0
  313. data/lib/hecks/runtime/dependency_planning.rb +11 -11
  314. data/lib/hecks/runtime/dispatcher.rb +283 -90
  315. data/lib/hecks/runtime/entity_element.rb +151 -67
  316. data/lib/hecks/runtime/entity_interpreter.rb +108 -82
  317. data/lib/hecks/runtime/errors.rb +19 -19
  318. data/lib/hecks/runtime/event.rb +6 -6
  319. data/lib/hecks/runtime/identity.rb +22 -22
  320. data/lib/hecks/runtime/instance.rb +39 -14
  321. data/lib/hecks/runtime/interpreting.rb +12 -12
  322. data/lib/hecks/runtime/invocation.rb +276 -0
  323. data/lib/hecks/runtime/loader.rb +14 -14
  324. data/lib/hecks/runtime/outbox.rb +23 -23
  325. data/lib/hecks/runtime/policy_interpreter.rb +54 -54
  326. data/lib/hecks/runtime/port_operation_interpreter.rb +22 -19
  327. data/lib/hecks/runtime/query_interpreter.rb +55 -64
  328. data/lib/hecks/runtime/reaction_invocation.rb +20 -20
  329. data/lib/hecks/runtime/read_model_interpreter.rb +40 -40
  330. data/lib/hecks/runtime/rebuild_sweep.rb +4 -4
  331. data/lib/hecks/runtime/reference_hop.rb +6 -6
  332. data/lib/hecks/runtime/refusal_wording.rb +92 -115
  333. data/lib/hecks/runtime/registry/saga_persistence.rb +21 -21
  334. data/lib/hecks/runtime/registry/verification.rb +36 -26
  335. data/lib/hecks/runtime/registry.rb +45 -28
  336. data/lib/hecks/runtime/remote_dispatcher.rb +38 -23
  337. data/lib/hecks/runtime/routing.rb +10 -153
  338. data/lib/hecks/runtime/saga_interpreter/correlation.rb +17 -17
  339. data/lib/hecks/runtime/saga_interpreter.rb +64 -64
  340. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  341. data/lib/hecks/runtime/tenant_check.rb +9 -9
  342. data/lib/hecks/runtime/tenant_scope.rb +5 -5
  343. data/lib/hecks/runtime/value/admission.rb +75 -30
  344. data/lib/hecks/runtime/value/coercion.rb +318 -127
  345. data/lib/hecks/runtime/value/entity_list_coercion.rb +34 -34
  346. data/lib/hecks/runtime/value.rb +21 -21
  347. data/lib/hecks/runtime.rb +7 -7
  348. data/lib/hecks/storehouse.rb +64 -64
  349. data/lib/hecks/version.rb +3 -3
  350. data/lib/hecks/vocabulary.rb +205 -4
  351. data/lib/hecks.rb +13 -11
  352. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +11 -11
  353. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +14 -12
  354. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +11 -11
  355. metadata +22 -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,45 +31,42 @@ 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
- ]
68
- # QualityControl WAS the first domain in this corpus to trigger an
69
- # `asks`/`tells` PORT OPERATION from a `policy`, and used to carry
70
- # two entries here for it — both GONE now, not just quieted:
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:
71
70
  #
72
71
  # `deaf_policy` (ClearOnPass, RefuseOnFail, RecordTheIssue,
73
72
  # RecordTheRefusal) went first: `emitted_events` below now reads an
@@ -82,7 +81,7 @@ module Hecks
82
81
  # never actually a `Naming`/`PolicyBuilder` defect, confirmed by
83
82
  # tracing the real dispatch path rather than assuming the comment
84
83
  # that used to sit here: `Naming.command_ref`'s bare-constant
85
- # rewrite DOES leave `trigger Ticket::IssueTracker::File` (aggregate,
84
+ # rewrite does leave `trigger Ticket::IssueTracker::File` (aggregate,
86
85
  # port, operation) as "Ticket::IssueTracker.File", a leftover `::`
87
86
  # past the aggregate — but `PolicyInterpreter#deliver` re-qualifies
88
87
  # every trigger with this domain's own name before dispatch
@@ -92,7 +91,7 @@ module Hecks
92
91
  # resolve_target`, PR #520, predating this entry's own removal) —
93
92
  # confirmed live: a real dispatch through `Ticket.Submit` fires
94
93
  # `IssueFiled`/`TicketFiled` exactly as declared. The actual gap was
95
- # entirely in THIS checker: `verbs_of` never enumerated a port
94
+ # entirely in this checker: `verbs_of` never enumerated a port
96
95
  # operation as a triggerable verb at all, and `policy_findings`
97
96
  # compared raw strings instead of `Naming.split_verb` triples the
98
97
  # way `handler_findings`'s own `unknown_dispatch` check already does
@@ -106,16 +105,20 @@ module Hecks
106
105
  # `hecksagon:`/`known_domains:` — both optional, both `nil`-safe
107
106
  # (every existing caller with no sibling hecksagon, or checking one
108
107
  # domain in isolation, behaves exactly as before). `hecksagon` is
109
- # 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
110
109
  # (see `emitted_events`'s own comment on why a caller that didn't
111
110
  # simply finds none, correctly). `known_domains` is the caller's
112
- # OWN corpus-wide view — every bluebook/hecksagon name it has
111
+ # own corpus-wide view — every bluebook/hecksagon name it has
113
112
  # booted anywhere, across every domain it has looked at, not just
114
113
  # this one — used only to catch a typo'd `across`/`uses_framework`
115
114
  # target; see `cross_domain_policy_findings`'s own comment for why
116
115
  # this can only ever be a corpus-scoped heuristic, never a general
117
116
  # correctness guarantee.
118
- 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)
119
122
  findings = []
120
123
  bluebook.aggregates.each do |aggregate|
121
124
  findings.concat(lifecycle_findings(aggregate, aggregate))
@@ -123,10 +126,68 @@ module Hecks
123
126
  end
124
127
  bluebook.process_managers.each { |process_manager| findings.concat(saga_findings(bluebook, process_manager)) }
125
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))
126
132
  findings
127
133
  end
128
134
 
129
- # ── 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) ──
130
191
 
131
192
  def lifecycle_findings(aggregate, declaring)
132
193
  lifecycle = declaring.lifecycle
@@ -198,7 +259,7 @@ module Hecks
198
259
  ).uniq
199
260
  end
200
261
 
201
- # Least fixpoint from the default state: an UNCONSTRAINED
262
+ # Least fixpoint from the default state: an unconstrained
202
263
  # transition always fires, from wherever the machine is ; a
203
264
  # constrained one fires once any of its named sources is reached.
204
265
  def reachable_states(lifecycle)
@@ -222,8 +283,8 @@ module Hecks
222
283
  reached
223
284
  end
224
285
 
225
- # A state with no OUTGOING declared path at all is exempt from the
226
- # 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
227
288
  # unconstrained transition" — the default state of a lifecycle
228
289
  # with only constrained transitions is legitimately allowed to sit
229
290
  # forever, since nothing about *entering* it via default implies
@@ -231,7 +292,7 @@ module Hecks
231
292
  # explicitly delivered somewhere.
232
293
  #
233
294
  # Scoped to `default` alone, and only when the lifecycle actually
234
- # declares real transitions elsewhere: an EMPTY lifecycle (no
295
+ # declares real transitions elsewhere: an empty lifecycle (no
235
296
  # transitions at all) doesn't get this exemption — that's not "a
236
297
  # machine whose entry point deliberately awaits external action,"
237
298
  # it's much more likely a lifecycle nobody finished wiring, and
@@ -249,7 +310,7 @@ module Hecks
249
310
 
250
311
  def saga_findings(bluebook, process_manager)
251
312
  emitted = emitted_events(bluebook)
252
- # (domain, aggregate, command) TRIPLES, not raw strings — see
313
+ # (domain, aggregate, command) triples, not raw strings — see
253
314
  # `handler_findings`'s own comment on the dispatch side for why:
254
315
  # `Naming.split_verb` is what makes an entity verb's two legitimate
255
316
  # spellings (`Naming.command_ref`'s own `::`-then-`.` rewrite vs.
@@ -285,11 +346,11 @@ module Hecks
285
346
  end
286
347
  end
287
348
 
288
- # ONE HANDLER'S OWN FINDINGS — deaf_handler, unknown_dispatch (one
349
+ # One handler's own findings — deaf_handler, unknown_dispatch (one
289
350
  # per dispatch), and unarmed_compensation (one per compensating
290
351
  # dispatch), pulled out of saga_findings' own handler loop; each
291
352
  # check reads only this handler plus the domain-wide emitted/verbs
292
- # sets saga_findings already resolved once, no state shared BETWEEN
353
+ # sets saga_findings already resolved once, no state shared between
293
354
  # handlers.
294
355
  def handler_findings(bluebook, process_manager, emitted, verbs, handler)
295
356
  findings = []
@@ -305,7 +366,7 @@ module Hecks
305
366
  end
306
367
 
307
368
  handler.dispatches.each do |dispatch|
308
- # ALWAYS THIS DOMAIN — same fix, same reason, as `SagaInterpreter
369
+ # **Always this domain** — same fix, same reason, as `SagaInterpreter
309
370
  # #qualified` (BUG#6). This used to guess: a dispatch whose own
310
371
  # `command_name` still carried a leftover `::` after `Naming.
311
372
  # command_ref`'s own rewrite was read as "already qualified" and
@@ -320,7 +381,7 @@ module Hecks
320
381
  # — instead of maintaining its own, independently-wrong copy of
321
382
  # the same guess.
322
383
  #
323
- # COMPARED AS A TRIPLE, NOT A STRING — `Naming.command_ref`'s
384
+ # Compared as a triple, not a string — `Naming.command_ref`'s
324
385
  # own rewrite of an entity reference (`Manifest::Slot::Fill`)
325
386
  # collapses to "Manifest::Slot.Fill" (`::` between aggregate and
326
387
  # entity, `.` before the command); `verbs_of`'s own entity
@@ -345,7 +406,7 @@ module Hecks
345
406
  "checker's scope, same as CommandRules#resolve_references")
346
407
  end
347
408
 
348
- # A `compensates` DECLARED WITH NOWHERE TO EVER FIRE — the exact
409
+ # A `compensates` declared with nowhere to ever fire — the exact
349
410
  # shape of the real bug this whole feature closes ("the
350
411
  # reversal was written and never armed"), caught at build/
351
412
  # model-check time instead of discovered in production. No
@@ -374,7 +435,7 @@ module Hecks
374
435
  end
375
436
 
376
437
  # A handler edge is only usable in the closure if it can actually
377
- # FIRE — REFUSED always can (it is a compensation trigger, not an
438
+ # fire — REFUSED always can (it is a compensation trigger, not an
378
439
  # event), and any other handler needs its event genuinely emitted.
379
440
  # Without this, a deaf handler's declared from_state -> to_state
380
441
  # pair reads as connected even though nothing can ever traverse
@@ -408,8 +469,8 @@ module Hecks
408
469
  emitted = emitted_events(bluebook)
409
470
  findings = []
410
471
 
411
- # `policy.event_name` (Naming.unqualified) — NOT `bare`, which only
412
- # 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
413
474
  # `on_event` carries a "." aggregate qualifier instead (PolicyBuilder
414
475
  # stores whatever was typed, verbatim — see `on "Account.
415
476
  # AccountFrozen"`), and `bare` left it untouched, silently comparing
@@ -429,13 +490,13 @@ module Hecks
429
490
  # "#{domain}::#{trigger_command}" — the same join `verbs_of` builds
430
491
  # independently, so the two spellings have to be compared as FQNs.
431
492
  #
432
- # COMPARED AS A TRIPLE, NOT A STRING — `handler_findings`'s own
493
+ # Compared as a triple, not a string — `handler_findings`'s own
433
494
  # `unknown_dispatch` check (BUG#6) already applies this fix for a
434
495
  # saga's dispatch; a policy's `trigger` needed the identical one. A
435
- # policy triggering an `asks`/`tells` PORT OPERATION (`Aggregate::
496
+ # policy triggering an `asks`/`tells` port operation (`Aggregate::
436
497
  # Port::Operation`, three colon-joined segments — `Naming.command_ref`'s
437
498
  # bare-constant rewrite turns this into "Aggregate::Port.Operation",
438
- # a leftover `::` past the aggregate) is a real, WORKING dispatch —
499
+ # a leftover `::` past the aggregate) is a real, working dispatch —
439
500
  # `PolicyInterpreter#deliver` qualifies it with this domain's own
440
501
  # name before `Naming.split_verb` ever sees it, and `split_verb`
441
502
  # already folds that leftover `::` into the dot-joined tail
@@ -458,17 +519,17 @@ module Hecks
458
519
 
459
520
  # ── cross-domain policies (Context Mapping) ───────────────────────
460
521
  #
461
- # `uses_framework "X"` already IS a Shared Kernel relationship — it
462
- # merges X's own bluebook into THIS registry, no boundary. A cross-
463
- # 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
464
525
  # relationship — it dispatches into X over real cross-Lambda RPC in
465
526
  # the Rust host (`rust/host/src/lambda_client.rs`). Neither is a new
466
- # word; this makes the CHOICE between them checked instead of a
527
+ # word; this makes the choice between them checked instead of a
467
528
  # prose comment nobody enforces (`examples/banking/bluebook/
468
529
  # banking.hecksagon`'s own hand-written note explaining why
469
530
  # Compliance is reached via `across`, never `uses_framework`).
470
531
  #
471
- # NO NEW KEYWORD ANYWHERE — ADR 0025 principle 1 ("one idea, one
532
+ # No new keyword anywhere — ADR 0025 principle 1 ("one idea, one
472
533
  # spelling") refuses a `relationship:`/`as:` argument that would
473
534
  # just restate, as a string, the fact the chosen keyword (
474
535
  # `uses_framework` vs `across`) already states completely. The
@@ -476,14 +537,15 @@ module Hecks
476
537
  # the finding's own name and this comment, and in prose docs — not
477
538
  # in the grammar.
478
539
  def cross_domain_policy_findings(policy, hecksagon, known_domains)
540
+ return expected_undelivered_findings(policy, hecksagon, known_domains) if policy.expect_undelivered
479
541
  return [] unless hecksagon # no sibling hecksagon loaded — nothing to check a relationship against.
480
542
 
481
543
  target = policy.target_domain
482
544
  findings = []
483
545
 
484
546
  if hecksagon.framework_members.include?(target)
485
- # SHARED KERNEL AND CUSTOMER/SUPPLIER ARE MUTUALLY EXCLUSIVE
486
- # 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
487
549
  # loaded in-process, right here"; `across` means "X is a
488
550
  # separate deployment, reached only by RPC." Declaring both is
489
551
  # either a pointless RPC to a domain already local, or a
@@ -495,7 +557,7 @@ module Hecks
495
557
  "relationship declarations contradict each other for the same " \
496
558
  "target domain")
497
559
  elsif hecksagon.subscriptions.none? { |subscribed| Naming.qualifier(subscribed) == target }
498
- # THIS IS WHAT FINALLY GIVES `subscribe` REAL TEETH — checked
560
+ # This is what finally gives `subscribe` real teeth — checked
499
561
  # here, at model-check time, still never routed at runtime
500
562
  # (nothing dispatches off a `subscribe` line; see hecksagon.md's
501
563
  # own "checked, not routed" section). ADR 0025 names `subscribe`
@@ -508,43 +570,67 @@ module Hecks
508
570
  "attach it in-process instead")
509
571
  end
510
572
 
511
- # TYPO DETECTION, DELIBERATELY WEAKER — `known_domains` can only
512
- # 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
513
575
  # consumer's own domain (this repo's own embryonaut/lifeadelics-
514
576
  # shaped case) lives in a genuinely separate repository this
515
577
  # corpus scan can never see, so a target this check cannot find
516
- # is "unknown to THIS corpus," never proof of a typo. Two real,
517
- # legitimate reasons a target is unresolvable genuinely
518
- # 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
519
580
  # deliberately to exercise the undelivered-reaction runtime path)
520
- # and real-but-external (a separate repository)both go in
521
- # `ALLOWED_FINDINGS`, the same judged-exception mechanism this
522
- # file already uses for ExternalSettlement, rather than a new
523
- # 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.
524
584
  if known_domains && !known_domains.include?(target)
525
585
  findings << Finding.new(kind: :unknown_target_domain, severity: :error, subject: policy.name,
526
586
  message: "across #{target.inspect} names a domain nowhere in the corpus " \
527
- "this check has booted — a typo, or a real domain intentionally " \
528
- "outside this corpus (undefined by design, or living in a " \
529
- "separate repository) belongs in ALLOWED_FINDINGS, named and " \
530
- "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")
531
590
  end
532
591
 
533
592
  findings
534
593
  end
535
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
+
536
622
  # ── shared enumeration ────────────────────────────────────────────
537
623
 
538
- # A PORT OPERATION EMITS TOO — the primary/driving port an adapter
624
+ # A port operation emits too — the primary/driving port an adapter
539
625
  # outside the bluebook calls through (see hecksagon_builder.rb) is a
540
626
  # second, real source of events, alongside a command's own `emits`.
541
- # Ports attach to the aggregate/bluebook from the SIBLING `.hecksagon`
627
+ # Ports attach to the aggregate/bluebook from the sibling `.hecksagon`
542
628
  # file, not this one — a caller that boots only the `.bluebook` (as
543
629
  # the fixtures under spec/fixtures/model_check/ do, having no
544
630
  # hecksagon at all) simply finds none, which is correct : nothing
545
631
  # can be deaf to an event that isn't even wired up yet.
546
632
  #
547
- # AN OUTBOUND OPERATION (`asks`) EMITS THROUGH A DIFFERENT DOOR — it
633
+ # An outbound operation (`asks`) emits through a different door — it
548
634
  # declares no `.emits` at all (`PortOperationBuilder#refuse_wrong_
549
635
  # words!` refuses one that tries), naming its two real endings
550
636
  # `.answers`/`.refuses` instead (`PortOperation#initialize`). Reading
@@ -552,7 +638,7 @@ module Hecks
552
638
  # method — real, live events a policy genuinely reacts to
553
639
  # (`Clearance.SuitePassed`/`SuiteFailed`, `Ticket.IssueFiled`/
554
640
  # `IssueFilingRefused`), reported as `deaf_policy` findings until this
555
- # read both. `.compact` because an INBOUND operation's `.answers`/
641
+ # read both. `.compact` because an inbound operation's `.answers`/
556
642
  # `.refuses` are always nil (there is no channel back to tell), which
557
643
  # would otherwise seed every emitted-events set with a stray nil.
558
644
  def emitted_events(bluebook)
@@ -566,7 +652,7 @@ module Hecks
566
652
  (aggregate_emits + chapter_emits).flatten.compact.uniq
567
653
  end
568
654
 
569
- # ONE OPERATION, EITHER OF ITS OWN SOURCES OF EVENTS — an inbound
655
+ # One operation, either of its own sources of events — an inbound
570
656
  # `tells` names its own via `.emits`; an outbound `asks` has none
571
657
  # (`PortOperationBuilder#refuse_wrong_words!` refuses one that
572
658
  # tries) and names its two real endings `.answers`/`.refuses`
@@ -592,13 +678,13 @@ module Hecks
592
678
  end
593
679
  end
594
680
 
595
- # AN AGGREGATE-OWNED PORT OPERATION IS A TRIGGERABLE VERB TOO
681
+ # An aggregate-owned port operation is a triggerable verb too
596
682
  # `ReactionInvocation#resolve_target`'s own port-operation branch
597
683
  # resolves one by the exact same two-segment tail shape ("Aggregate::
598
684
  # Port.Operation", the aggregate then the port then the operation,
599
685
  # dot-joined past the domain) an entity command uses, checked first,
600
686
  # same order `Dispatcher#dispatch` already resolves a live verb in.
601
- # Only an aggregate's OWN ports (`aggregate.ports`) are in scope here
687
+ # Only an aggregate's own ports (`aggregate.ports`) are in scope here
602
688
  # — a policy's `trigger` always names one aggregate, never a chapter-
603
689
  # level port with no owner to address through.
604
690
  def port_verbs_of(bluebook)
@@ -611,7 +697,7 @@ module Hecks
611
697
  end
612
698
  end
613
699
 
614
- # EVERY TRIGGERABLE VERB, AS A TRIPLE — `verbs_of` (ordinary/entity
700
+ # Every triggerable verb, as a triple — `verbs_of` (ordinary/entity
615
701
  # commands) plus `port_verbs_of` (port operations), each parsed
616
702
  # through `Naming.split_verb` so a caller never has to compare two
617
703
  # spellings of the same verb as strings (see `policy_findings`'s own
@@ -1,24 +1,24 @@
1
1
  module Hecks
2
2
  module Bluebook
3
- # WHICH REGEXES A BLUEBOOK MAY SAY.
3
+ # **Which regexes a bluebook may say**.
4
4
  #
5
5
  # A `pattern:` is a fact about a value, carried in a bluebook — declared
6
6
  # data, not Ruby code, so it must not lean on what any one engine happens
7
7
  # to accept. Regex engines disagree in two different ways :
8
8
  #
9
- # ONLY A BACKTRACKING ENGINE CAN MATCH IT — lookahead, lookbehind,
9
+ # only a backtracking engine can match it — lookahead, lookbehind,
10
10
  # backreferences, atomic groups, possessive quantifiers. None of these
11
11
  # can be matched in linear time, and linear-time engines refuse them
12
12
  # outright. Refused here for the same reason.
13
13
  #
14
- # EVERY ENGINE PARSES IT AND THEY MEAN DIFFERENT THINGS — the dangerous
14
+ # every engine parses it and they mean different things — the dangerous
15
15
  # half, because nothing errors. `\d` `\w` `\s` are ASCII in some engines
16
16
  # and Unicode in others ; `[:digit:]` and friends flip the same way in
17
17
  # the other direction. Both families are refused, and a domain spells
18
18
  # the range it means.
19
19
  #
20
20
  # What remains — explicit ranges, alternation, quantifiers, anchors,
21
- # groups — reads identically everywhere, with `^` and `$` as LINE anchors
21
+ # groups — reads identically everywhere, with `^` and `$` as line anchors
22
22
  # (Ruby's reading). The evidence is spec/corpus/fixtures/patterns.json.
23
23
  module PatternSubset
24
24
  Rejection = Struct.new(:construct, :reason)
@@ -57,13 +57,13 @@ module Hecks
57
57
 
58
58
  # nil when the pattern is admitted, a Rejection when it is not.
59
59
  #
60
- # A CHARACTER WALK, deliberately plain : the subset is defined by this
60
+ # A character walk, deliberately plain : the subset is defined by this
61
61
  # walk, and a cleverer spelling would hide what it admits. An escaped
62
- # construct is a LITERAL, not a violation — `\(\?=` is the three
62
+ # construct is a literal, not a violation — `\(\?=` is the three
63
63
  # characters "(?=" and says nothing about lookahead — which is why this
64
64
  # steps over each backslash pair rather than matching the pattern as a
65
65
  # whole.
66
- # One character walk is the subset's DEFINITION (see the method
66
+ # One character walk is the subset's definition (see the method
67
67
  # comment above): each construct-check is a branch in a single
68
68
  # ordered pass sharing `index`/`in_class`/`class_start`. Splitting
69
69
  # the branches into separate methods would force those three cursor
@@ -75,7 +75,7 @@ module Hecks
75
75
  def validate(pattern)
76
76
  chars = pattern.to_s.chars
77
77
  index = 0
78
- # A CHARACTER-CLASS INTERIOR IS A DIFFERENT ALPHABET : inside `[...]`,
78
+ # A character-class interior is a different alphabet : inside `[...]`,
79
79
  # `*`, `+`, `?`, `(`, `?` are literal characters, not quantifiers or
80
80
  # group syntax — `[*+]` means "a literal asterisk or plus". `]` is
81
81
  # only the class's close when it isn't the first character after `[`
@@ -132,7 +132,7 @@ module Hecks
132
132
  nil
133
133
  end
134
134
 
135
- # SPELLED OUT, not derived from the key : these strings are the refusal
135
+ # Spelled out, not derived from the key : these strings are the refusal
136
136
  # a caller reads.
137
137
  CONSTRUCTS = {
138
138
  backreference: "backreference",
@@ -1,5 +1,5 @@
1
- # GENERATED — projected from the language's own Policy aggregate.
2
- # DO NOT EDIT: the holding half is rendered, and Behaviour::Policy
1
+ # Generated — projected from the language's own Policy aggregate.
2
+ # Do not edit: the holding half is rendered, and Behaviour::Policy
3
3
  # is where anything hand-written belongs.
4
4
  require_relative "behaviour/policy"
5
5
 
@@ -10,28 +10,30 @@ module Hecks
10
10
  include Behaviour::Policy
11
11
 
12
12
  emits_ir(
13
- name: :name,
14
- on_event: :on_event,
15
- trigger_command: :trigger_command,
16
- target_domain: :target_domain,
17
- where: :where,
18
- for_each: :for_each,
19
- with_spec: -> { with_spec.map { |key, value| [key.to_s, Bluebook.render_value(value)] } },
20
- where_ast: -> { where_ast }
13
+ name: :name,
14
+ on_event: :on_event,
15
+ trigger_command: :trigger_command,
16
+ target_domain: :target_domain,
17
+ expect_undelivered: :expect_undelivered,
18
+ where: :where,
19
+ for_each: :for_each,
20
+ with_spec: -> { with_spec.map { |key, value| [key.to_s, Bluebook.render_value(value)] } },
21
+ where_ast: -> { where_ast }
21
22
  )
22
23
 
23
- attr_reader :name, :on_event, :trigger_command, :target_domain, :where, :for_each, :with_spec
24
+ attr_reader :name, :on_event, :trigger_command, :target_domain, :expect_undelivered, :where, :for_each, :with_spec
24
25
 
25
- # AGGREGATE, DECLARED AND DELIBERATELY OFF THE WIRE
26
+ # Aggregate, declared and deliberately off the wire
26
27
  # the wire format is a pinned contract, and it does not carry
27
28
  # where a policy was written before the builder hoisted it
28
29
  attr_accessor :aggregate
29
30
 
30
- def initialize(name:, on_event: nil, trigger_command: nil, target_domain: nil, where: nil, for_each: nil, with_spec: [], aggregate: nil)
31
+ def initialize(name:, on_event: nil, trigger_command: nil, target_domain: nil, expect_undelivered: false, where: nil, for_each: nil, with_spec: [], aggregate: nil)
31
32
  @name = name.to_s
32
33
  @on_event = on_event
33
34
  @trigger_command = trigger_command
34
35
  @target_domain = target_domain
36
+ @expect_undelivered = expect_undelivered.to_s == "true"
35
37
  @where = where
36
38
  @for_each = for_each
37
39
  @with_spec = with_spec