hecks 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (373) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +72 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +83 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
  7. data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
  9. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  10. data/lib/hecks/adapters/driven/heki.rb +106 -9
  11. data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
  12. data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
  13. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  15. data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
  16. data/lib/hecks/adapters/driven/lambda.rb +82 -35
  17. data/lib/hecks/adapters/driven/local_storage.rb +83 -10
  18. data/lib/hecks/adapters/driven/memory.rb +205 -9
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -1
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  24. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  25. data/lib/hecks/adapters/driven/prism.rb +50 -11
  26. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  31. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  32. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  33. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  34. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  35. data/lib/hecks/adapters/driven.rb +6 -4
  36. data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
  37. data/lib/hecks/behaviors/dsl.rb +60 -2
  38. data/lib/hecks/behaviors/expectations.rb +190 -29
  39. data/lib/hecks/behaviors/ir.rb +12 -1
  40. data/lib/hecks/behaviors/rspec.rb +9 -1
  41. data/lib/hecks/behaviors/runner.rb +21 -2
  42. data/lib/hecks/behaviors.rb +9 -1
  43. data/lib/hecks/bluebook/aggregate.rb +43 -13
  44. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
  45. data/lib/hecks/bluebook/assembly/build.rb +26 -5
  46. data/lib/hecks/bluebook/assembly/contract.rb +98 -23
  47. data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
  48. data/lib/hecks/bluebook/assembly/marks.rb +159 -30
  49. data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
  50. data/lib/hecks/bluebook/assembly.rb +32 -14
  51. data/lib/hecks/bluebook/attribute.rb +26 -12
  52. data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
  53. data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
  54. data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
  55. data/lib/hecks/bluebook/behaviour/command.rb +55 -25
  56. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
  57. data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
  58. data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
  59. data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
  60. data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
  61. data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
  62. data/lib/hecks/bluebook/behaviour/query.rb +6 -1
  63. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
  64. data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
  65. data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
  66. data/lib/hecks/bluebook/capabilities.rb +27 -0
  67. data/lib/hecks/bluebook/chapter.rb +51 -9
  68. data/lib/hecks/bluebook/command.rb +62 -17
  69. data/lib/hecks/bluebook/domain_port.rb +34 -9
  70. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  71. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  72. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +282 -123
  73. data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
  74. data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
  75. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
  76. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
  77. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  78. data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
  79. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  80. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  81. data/lib/hecks/bluebook/dsl/entity_builder.rb +191 -61
  82. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  83. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
  84. data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
  85. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  86. data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
  87. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  88. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  89. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +111 -47
  90. data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
  91. data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
  92. data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
  93. data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
  94. data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
  95. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  96. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  97. data/lib/hecks/bluebook/entity.rb +40 -11
  98. data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
  99. data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
  100. data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
  101. data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
  102. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
  103. data/lib/hecks/bluebook/expression/resolver.rb +369 -128
  104. data/lib/hecks/bluebook/hexagon.rb +35 -1
  105. data/lib/hecks/bluebook/lifecycle.rb +12 -1
  106. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
  107. data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
  108. data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
  109. data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
  110. data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
  111. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
  112. data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
  113. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
  114. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
  115. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
  116. data/lib/hecks/bluebook/meta_validator.rb +235 -139
  117. data/lib/hecks/bluebook/model_check.rb +434 -104
  118. data/lib/hecks/bluebook/pattern_subset.rb +32 -10
  119. data/lib/hecks/bluebook/policy.rb +15 -13
  120. data/lib/hecks/bluebook/process_manager.rb +27 -14
  121. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  122. data/lib/hecks/bluebook/project_loader.rb +40 -0
  123. data/lib/hecks/bluebook/project_register.rb +50 -6
  124. data/lib/hecks/bluebook/query.rb +31 -4
  125. data/lib/hecks/bluebook/read_model.rb +35 -15
  126. data/lib/hecks/bluebook/reference.rb +26 -13
  127. data/lib/hecks/bluebook/smoke_test.rb +46 -23
  128. data/lib/hecks/bluebook/synthesizer.rb +46 -12
  129. data/lib/hecks/bluebook/translation.rb +34 -5
  130. data/lib/hecks/bluebook/value_object.rb +29 -11
  131. data/lib/hecks/bluebook.rb +5 -6
  132. data/lib/hecks/codemod.rb +138 -50
  133. data/lib/hecks/construct.rb +21 -7
  134. data/lib/hecks/corpus.rb +438 -0
  135. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  136. data/lib/hecks/doc/reference.rb +200 -31
  137. data/lib/hecks/embryonaut_bluebook.rb +38 -15
  138. data/lib/hecks/facade/cli_door.rb +69 -10
  139. data/lib/hecks/facade/cli_runner.rb +105 -24
  140. data/lib/hecks/facade/command_request.rb +23 -0
  141. data/lib/hecks/facade/handle.rb +155 -35
  142. data/lib/hecks/facade/json_door.rb +106 -25
  143. data/lib/hecks/facade/surface/aggregate_door.rb +50 -27
  144. data/lib/hecks/facade/surface/chapter.rb +26 -17
  145. data/lib/hecks/facade/surface.rb +16 -3
  146. data/lib/hecks/facade.rb +15 -4
  147. data/lib/hecks/forms/app.rb +46 -30
  148. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  149. data/lib/hecks/forms/field_renderer.rb +142 -6
  150. data/lib/hecks/forms/field_shape.rb +183 -19
  151. data/lib/hecks/forms/html.rb +51 -7
  152. data/lib/hecks/forms/index_renderer.rb +14 -2
  153. data/lib/hecks/forms/page.rb +14 -0
  154. data/lib/hecks/forms/params.rb +120 -23
  155. data/lib/hecks/forms/port_argument.rb +14 -2
  156. data/lib/hecks/forms/query_form_renderer.rb +65 -2
  157. data/lib/hecks/forms/record_renderer.rb +60 -2
  158. data/lib/hecks/forms/record_table.rb +28 -1
  159. data/lib/hecks/forms/reference_options.rb +24 -0
  160. data/lib/hecks/forms/value_object_shape.rb +13 -3
  161. data/lib/hecks/forms.rb +24 -4
  162. data/lib/hecks/fqn.rb +59 -1
  163. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  164. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  165. data/lib/hecks/framework/oidc.json +15 -0
  166. data/lib/hecks/framework.rb +79 -25
  167. data/lib/hecks/freezer.rb +27 -11
  168. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
  169. data/lib/hecks/fuzzing/combination_miner.rb +178 -0
  170. data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
  171. data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
  172. data/lib/hecks/fuzzing/differential.rb +192 -0
  173. data/lib/hecks/fuzzing/domain_generator.rb +871 -0
  174. data/lib/hecks/fuzzing/era_boundary.rb +68 -19
  175. data/lib/hecks/fuzzing/form_census.rb +121 -20
  176. data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
  177. data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
  178. data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
  179. data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
  180. data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
  181. data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
  182. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
  183. data/lib/hecks/fuzzing/properties/guards.rb +86 -42
  184. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
  185. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
  186. data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
  187. data/lib/hecks/fuzzing/properties/querying.rb +82 -28
  188. data/lib/hecks/fuzzing/properties.rb +84 -51
  189. data/lib/hecks/fuzzing/qa_settings.rb +164 -0
  190. data/lib/hecks/fuzzing/replay.rb +241 -133
  191. data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
  192. data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
  193. data/lib/hecks/fuzzing/self_consistency.rb +315 -137
  194. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
  195. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
  196. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
  197. data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
  198. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
  199. data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
  200. data/lib/hecks/fuzzing/shrinker.rb +309 -0
  201. data/lib/hecks/fuzzing/structural_skips.rb +37 -130
  202. data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
  203. data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
  204. data/lib/hecks/fuzzing/value_generator.rb +110 -22
  205. data/lib/hecks/fuzzing.rb +5 -0
  206. data/lib/hecks/grammar/evolve.rb +188 -12
  207. data/lib/hecks/grammar.rb +53 -7
  208. data/lib/hecks/ir.rb +51 -20
  209. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  210. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  211. data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
  212. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  213. data/lib/hecks/language/oidc.json +5 -0
  214. data/lib/hecks/literal.rb +41 -9
  215. data/lib/hecks/naming.rb +112 -31
  216. data/lib/hecks/ports/access_control.rb +53 -2
  217. data/lib/hecks/ports/agent/answers.rb +83 -6
  218. data/lib/hecks/ports/agent.rb +119 -35
  219. data/lib/hecks/ports/authentication.rb +44 -4
  220. data/lib/hecks/ports/authorization.rb +50 -11
  221. data/lib/hecks/ports/clock.rb +42 -23
  222. data/lib/hecks/ports/extraction.rb +16 -0
  223. data/lib/hecks/ports/identity_assignment.rb +23 -2
  224. data/lib/hecks/ports/identity_generation.rb +17 -3
  225. data/lib/hecks/ports/identity_resolution.rb +17 -1
  226. data/lib/hecks/ports/key_vault.port +6 -0
  227. data/lib/hecks/ports/key_vault.rb +58 -0
  228. data/lib/hecks/ports/loading.rb +4 -0
  229. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  230. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  231. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  232. data/lib/hecks/ports/persistence/execution.rb +4 -0
  233. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  234. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  235. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  236. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  237. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  238. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  239. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +34 -5
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  250. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
  251. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  252. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +95 -6
  253. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  254. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
  255. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  256. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  257. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  258. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +48 -8
  259. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  260. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  261. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  262. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +58 -19
  263. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
  264. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  265. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -1
  266. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  267. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  268. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  269. data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
  270. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  271. data/lib/hecks/ports/persistence.rb +36 -1
  272. data/lib/hecks/ports/projection.rb +61 -7
  273. data/lib/hecks/ports/query/in_memory.rb +41 -8
  274. data/lib/hecks/ports/query/ordering.rb +21 -6
  275. data/lib/hecks/ports/query.rb +35 -0
  276. data/lib/hecks/ports.rb +1 -0
  277. data/lib/hecks/projections/bootstrap_table.rb +147 -0
  278. data/lib/hecks/projections/diagrams.rb +315 -79
  279. data/lib/hecks/projections/glossary/html.rb +91 -3
  280. data/lib/hecks/projections/glossary/markdown.rb +50 -3
  281. data/lib/hecks/projections/glossary/mermaid.rb +49 -1
  282. data/lib/hecks/projections/glossary/sections.rb +1 -1
  283. data/lib/hecks/projections/glossary/sentences.rb +110 -5
  284. data/lib/hecks/projections/glossary.rb +173 -31
  285. data/lib/hecks/projections/ir.rb +1 -1
  286. data/lib/hecks/projections/model/deviations.rb +62 -17
  287. data/lib/hecks/projections/model.rb +75 -21
  288. data/lib/hecks/projections/oidc.rb +45 -8
  289. data/lib/hecks/projections/parser_table.rb +53 -12
  290. data/lib/hecks/projections/reference.rb +15 -3
  291. data/lib/hecks/projections/rust_vocabulary.rb +646 -0
  292. data/lib/hecks/projections/shape.rb +10 -2
  293. data/lib/hecks/projections/statements.rb +69 -22
  294. data/lib/hecks/projections/vocabulary.rb +26 -9
  295. data/lib/hecks/projections.rb +5 -3
  296. data/lib/hecks/projector/cli_projector.rb +245 -37
  297. data/lib/hecks/projector/docs_projector.rb +154 -28
  298. data/lib/hecks/projector/exporter.rb +104 -29
  299. data/lib/hecks/projector/ir_projector.rb +7 -1
  300. data/lib/hecks/projector/narrate_projector.rb +144 -23
  301. data/lib/hecks/projector/target.rb +42 -18
  302. data/lib/hecks/projector.rb +86 -18
  303. data/lib/hecks/query_ir.rb +94 -47
  304. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  305. data/lib/hecks/query_specification/common/comparison.rb +109 -27
  306. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  307. data/lib/hecks/query_specification/common/null_policy.rb +61 -15
  308. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  309. data/lib/hecks/query_specification/common/options.rb +25 -0
  310. data/lib/hecks/query_specification/field_path.rb +69 -15
  311. data/lib/hecks/query_specification/hop_path.rb +57 -20
  312. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  313. data/lib/hecks/rendering.rb +9 -3
  314. data/lib/hecks/router/namespace_installer.rb +16 -3
  315. data/lib/hecks/router.rb +56 -1
  316. data/lib/hecks/runtime/aggregate_lock.rb +20 -11
  317. data/lib/hecks/runtime/boot_gates.rb +21 -3
  318. data/lib/hecks/runtime/caller.rb +40 -8
  319. data/lib/hecks/runtime/capability_graph.rb +13 -2
  320. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
  321. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
  322. data/lib/hecks/runtime/command_interpreter.rb +195 -113
  323. data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
  324. data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
  325. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  326. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  327. data/lib/hecks/runtime/command_rules/references.rb +145 -55
  328. data/lib/hecks/runtime/command_rules.rb +3 -0
  329. data/lib/hecks/runtime/dependency_planning.rb +56 -11
  330. data/lib/hecks/runtime/dispatcher.rb +263 -92
  331. data/lib/hecks/runtime/entity_element.rb +304 -67
  332. data/lib/hecks/runtime/entity_interpreter.rb +149 -88
  333. data/lib/hecks/runtime/errors.rb +37 -23
  334. data/lib/hecks/runtime/event.rb +15 -10
  335. data/lib/hecks/runtime/identity.rb +91 -23
  336. data/lib/hecks/runtime/instance.rb +103 -18
  337. data/lib/hecks/runtime/interpreting.rb +24 -16
  338. data/lib/hecks/runtime/invocation.rb +358 -0
  339. data/lib/hecks/runtime/loader.rb +105 -19
  340. data/lib/hecks/runtime/outbox.rb +164 -26
  341. data/lib/hecks/runtime/policy_interpreter.rb +73 -60
  342. data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
  343. data/lib/hecks/runtime/query_interpreter.rb +93 -74
  344. data/lib/hecks/runtime/reaction_invocation.rb +73 -28
  345. data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
  346. data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
  347. data/lib/hecks/runtime/reference_hop.rb +48 -6
  348. data/lib/hecks/runtime/refusal_wording.rb +142 -115
  349. data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
  350. data/lib/hecks/runtime/registry/verification.rb +153 -28
  351. data/lib/hecks/runtime/registry.rb +202 -32
  352. data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
  353. data/lib/hecks/runtime/routing.rb +36 -154
  354. data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
  355. data/lib/hecks/runtime/saga_interpreter.rb +90 -76
  356. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  357. data/lib/hecks/runtime/tenant_check.rb +33 -13
  358. data/lib/hecks/runtime/tenant_scope.rb +23 -5
  359. data/lib/hecks/runtime/value/admission.rb +75 -30
  360. data/lib/hecks/runtime/value/coercion.rb +555 -142
  361. data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
  362. data/lib/hecks/runtime/value.rb +71 -21
  363. data/lib/hecks/runtime.rb +39 -7
  364. data/lib/hecks/storehouse.rb +368 -72
  365. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  366. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  367. data/lib/hecks/version.rb +3 -3
  368. data/lib/hecks/vocabulary.rb +205 -4
  369. data/lib/hecks.rb +91 -11
  370. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
  371. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
  372. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
  373. metadata +28 -2
@@ -0,0 +1,358 @@
1
+ require_relative "errors"
2
+ require_relative "refusal_wording"
3
+ require_relative "routing"
4
+ require_relative "../rendering"
5
+
6
+ module Hecks
7
+ module Runtime
8
+ # **One dispatch, as data** — the verb, the receiver it is addressed to
9
+ # (`target`, a `Routing::Envelope` or nil), and every fact the caller did
10
+ # or did not offer. Built once per dispatch by `Invocation.from_call`, the
11
+ # only place the runtime interprets the shape of a call (`to:` vs `with:`
12
+ # vs the flat facts hash `dispatch_flat` carries, a port operation's
13
+ # reference attribute
14
+ # lifted into `to:`). `Runtime::Routing.envelope`/`.payload` delegate here
15
+ # and no longer hold that logic themselves.
16
+ #
17
+ # `facts` maps a fact name to exactly one of:
18
+ #
19
+ # - `Invocation::Absent` — the key was never offered (every declared
20
+ # attribute the caller left out appears so)
21
+ # - `Invocation::Null` — the key was offered with an explicit nil
22
+ # - `Invocation::Present` — the key was offered with a value
23
+ #
24
+ # Keys are kept exactly as offered: `with:` keys are symbolized (as they
25
+ # always were), a flat facts hash keeps whatever key the wire used. Undeclared keys a caller
26
+ # offered are kept too — refusing them is
27
+ # still `refuse_unknown_arguments`' job, a dispatch step, not this one.
28
+ #
29
+ # I1 changes no behavior: every interpreter still reads `ctx.args`,
30
+ # which is `#to_args` — the same Hash `Routing.payload` returns.
31
+ # Roadmap I2 moves the Ruby `decode_arguments` step onto `facts` itself.
32
+ Invocation = Data.define(:verb, :target, :facts)
33
+
34
+ # The fact markers, the fact readers, and `from_call` — see above.
35
+ class Invocation
36
+ # A fact the caller offered with a real (non-nil) value.
37
+ Present = Data.define(:value) do
38
+ def inspect = "#<Invocation::Present #{value.inspect}>"
39
+ alias_method :to_s, :inspect
40
+ end
41
+
42
+ # The class of the two frozen marker singletons below — never
43
+ # instantiated anywhere else.
44
+ class Marker
45
+ # @param name [String] the marker's own label, used by #inspect/#to_s
46
+ def initialize(name)
47
+ @name = name
48
+ freeze
49
+ end
50
+
51
+ def inspect = "Invocation::#{@name}"
52
+ alias to_s inspect
53
+ end
54
+ private_constant :Marker
55
+
56
+ Absent = Marker.new("Absent")
57
+ Null = Marker.new("Null")
58
+
59
+ # @param verb [String] the fully qualified verb this invocation dispatches
60
+ # @param target [Routing::Envelope, nil] the resolved receiver, or nil when
61
+ # the facts carry the identity themselves
62
+ # @param facts [Hash{String, Symbol => Object}] every candidate fact, keyed by
63
+ # offered (or declared) name; frozen, deep-duped, before being stored
64
+ def initialize(verb:, target:, facts:)
65
+ super(verb: verb, target: target, facts: facts.dup.freeze)
66
+ end
67
+
68
+ # Reads the fact recorded under `name`.
69
+ #
70
+ # The fact recorded under `name` — `Absent` for a name never offered
71
+ # and never declared either.
72
+ #
73
+ # @param name [String, Symbol] the fact name to look up
74
+ # @return [Invocation::Present, Invocation::Null, Invocation::Absent] the
75
+ # recorded marker for `name`
76
+ def fact(name) = facts.fetch(name, Absent)
77
+
78
+ # Reports whether `name` was offered with a real, non-nil value.
79
+ #
80
+ # @param name [String, Symbol] the fact name to check
81
+ # @return [Boolean] true if `name` was offered with a non-nil value
82
+ def present?(name) = fact(name).is_a?(Present)
83
+
84
+ # Reports whether `name` was offered with an explicit nil.
85
+ #
86
+ # @param name [String, Symbol] the fact name to check
87
+ # @return [Boolean] true if `name` was offered with an explicit nil
88
+ def null?(name) = fact(name).equal?(Null)
89
+
90
+ # Reports whether `name` was never offered.
91
+ #
92
+ # @param name [String, Symbol] the fact name to check
93
+ # @return [Boolean] true if `name` was never offered (and, if declared, left out)
94
+ def absent?(name) = fact(name).equal?(Absent)
95
+
96
+ # A Present fact's value; nil for an explicit Null. Raises KeyError for
97
+ # an Absent fact — "never offered" has no value, and answering nil
98
+ # would re-conflate it with an explicit null, the very ambiguity this
99
+ # type exists to remove. Ask `absent?`/`present?` first.
100
+ #
101
+ # @param name [String, Symbol] the fact name to read
102
+ # @return [Object, nil] the offered value; nil for an explicit Null fact
103
+ # @raise [KeyError] if `name` names an Absent fact (never offered)
104
+ def value(name)
105
+ case (found = fact(name))
106
+ when Present then found.value
107
+ when Null then nil
108
+ else raise KeyError, "#{verb} was not given #{name.inspect}"
109
+ end
110
+ end
111
+
112
+ # The flat args hash, byte for byte what `Routing.payload` returned
113
+ # before this type existed: offered keys in offered order, Absent keys
114
+ # omitted, Null keys mapped to nil. A fresh Hash every call.
115
+ #
116
+ # @return [Hash{String, Symbol => Object}] the offered facts by name; a fact
117
+ # offered as nil is kept as nil
118
+ def to_args
119
+ facts.each_with_object({}) do |(name, found), args|
120
+ next if found.equal?(Absent)
121
+
122
+ args[name] = found.equal?(Null) ? nil : found.value
123
+ end
124
+ end
125
+
126
+ class << self
127
+ # The one reading of a call's shape. `receiver:` picks which of the
128
+ # three dispatch shapes this is, because each has always checked its
129
+ # parts in its own order and a malformed call's refusal depends on
130
+ # that order:
131
+ #
132
+ # :aggregate — facts first (`with:` checks), then `to:`
133
+ # :entity — `to:` first, then the block (which resolves the
134
+ # entity chain, UnknownVerb), then facts
135
+ # :port — a reference attribute / identity field lifted into
136
+ # `to:`, then `to:` (which a port operation requires),
137
+ # then facts
138
+ #
139
+ # The block answers the declaring command or port operation (anything
140
+ # with `hecks_name` and `attributes`); it is called exactly once, at
141
+ # the point in that order where the caller always resolved it.
142
+ # `aggregate:` is the owning aggregate construct, read for `:port`
143
+ # only.
144
+ #
145
+ # @param verb [String] the fully qualified verb being dispatched
146
+ # @param to [String, Hash, nil] a bare aggregate identity, or a Hash with
147
+ # `aggregate:` and `entity:`/`entities:`; nil for no explicit receiver
148
+ # @param with [Hash, nil] the command's facts, keyed by attribute name; nil
149
+ # when the caller offers `flat` instead
150
+ # @param flat [Hash] the command's facts as a flat args hash, used when `with`
151
+ # is nil; for `:port`, may also carry the receiver identity under a
152
+ # reference attribute's own name
153
+ # @param receiver [Symbol] which of the three dispatch shapes this is:
154
+ # `:aggregate`, `:entity` or `:port`
155
+ # @param entity_depth [Integer] the number of entity-hop identities `to:`
156
+ # must carry, for `:entity` only
157
+ # @param aggregate [Bluebook::Aggregate, nil] the owning aggregate construct,
158
+ # read for `:port` only
159
+ # @yieldreturn [Class] the declaring command or port-operation class (anything
160
+ # with `hecks_name` and `attributes`)
161
+ # @return [Runtime::Invocation] the built invocation
162
+ # @raise [Runtime::TypeMismatch] if `to:` is malformed, its entity count is
163
+ # wrong, or (`:port` only) the operation resolves no receiving aggregate
164
+ # @raise [Runtime::UnknownArgument] if `with:` names an attribute the command
165
+ # does not declare
166
+ # @raise [Runtime::AbsentArgument] if `with:` omits a required attribute
167
+ # @raise [ArgumentError] if `receiver` is none of `:aggregate`, `:entity` or `:port`
168
+ def from_call(verb, to:, with:, flat:, receiver: :aggregate, entity_depth: 0, aggregate: nil, &declaring)
169
+ case receiver
170
+ when :aggregate
171
+ command = declaring.call
172
+ facts = facts_for(command, with: with, flat: flat)
173
+ new(verb: verb, target: route(to), facts: facts)
174
+ when :entity
175
+ target = route(to, entity_depth: entity_depth)
176
+ command = declaring.call
177
+ new(verb: verb, target: target, facts: facts_for(command, with: with, flat: flat))
178
+ when :port
179
+ port_call(verb, aggregate, declaring.call, to: to, with: with, flat: flat)
180
+ else
181
+ raise ArgumentError, "unknown receiver #{receiver.inspect}"
182
+ end
183
+ end
184
+
185
+ # `to:` as a `Routing::Envelope`, or nil when no `to:` was given.
186
+ #
187
+ # @param to [String, Hash, nil] a bare aggregate identity, or a Hash with
188
+ # `aggregate:` and `entity:`/`entities:`; nil for no receiver
189
+ # @param entity_depth [Integer] the number of entity-hop identities `to`
190
+ # must carry
191
+ # @return [Routing::Envelope, nil] the resolved envelope, or nil when `to`
192
+ # is nil
193
+ # @raise [Runtime::TypeMismatch] if `to` is malformed, or its entity count
194
+ # does not match `entity_depth`
195
+ def route(to, entity_depth: 0)
196
+ return nil if to.nil?
197
+
198
+ aggregate, entities = to.is_a?(Hash) ? envelope_hash(to) : scalar_envelope(to)
199
+
200
+ raise TypeMismatch, "to: must name the receiving aggregate identity" if aggregate.nil? || aggregate.to_s.empty?
201
+ if entities.size != entity_depth
202
+ raise TypeMismatch,
203
+ "to: for an entity command needs #{entity_depth} entity " \
204
+ "#{entity_depth == 1 ? 'identity' : 'identities'} after the aggregate — got #{entities.size}"
205
+ end
206
+ raise TypeMismatch, "to: contains a blank entity identity" if entities.any? do |identity|
207
+ identity.nil? || identity.to_s.empty?
208
+ end
209
+
210
+ Routing::Envelope.new(aggregate: aggregate, entities: entities)
211
+ end
212
+
213
+ # The offered facts for `declaring`, as Absent/Null/Present — offered
214
+ # keys first, in offered order, then every declared attribute that was
215
+ # not offered, as Absent.
216
+ #
217
+ # @param declaring [Class] the command or port-operation class (anything
218
+ # with `hecks_name` and `attributes`) whose declared attributes are read
219
+ # @param with [Hash, nil] the command's facts, keyed by attribute name; nil
220
+ # when the caller offers `flat` instead
221
+ # @param flat [Hash] the command's facts as a flat args hash, used when `with`
222
+ # is nil
223
+ # @return [Hash{String, Symbol => Invocation::Absent, Invocation::Null,
224
+ # Invocation::Present}] each candidate fact keyed by its offered (or
225
+ # declared) name
226
+ # @raise [Runtime::TypeMismatch] if both `with` and a non-empty `flat` are
227
+ # given, or if `with` is not a Hash
228
+ # @raise [Runtime::UnknownArgument] if `with:` names an attribute `declaring`
229
+ # does not declare
230
+ # @raise [Runtime::AbsentArgument] if `with:` omits a required attribute
231
+ def facts_for(declaring, with:, flat:)
232
+ offered = offered_facts(declaring, with: with, flat: flat)
233
+ facts = offered.each_with_object({}) do |(name, value), found|
234
+ found[name] = value.nil? ? Null : Present.new(value: value)
235
+ end
236
+ declaring.attributes.each do |attribute|
237
+ name = attribute.name.to_sym
238
+ facts[name] = Absent unless facts.key?(name)
239
+ end
240
+ facts
241
+ end
242
+
243
+ private
244
+
245
+ # **The port operation shape** — moved from `Dispatcher#port_invocation`.
246
+ #
247
+ # A Reference-typed attribute naming the owning aggregate is routing,
248
+ # not a fact: lifted out of the flat facts into `to:` when no `to:`
249
+ # was given. A `to:`-declared operation carries no Reference
250
+ # attribute at all (PortOperationBuilder#initialize's own comment),
251
+ # so its receiver is read — not removed — from the plain attribute
252
+ # named for the owner's first `identified_by` field: that attribute
253
+ # is still a declared external fact `refuse_absent_arguments`
254
+ # expects to find (Rust's own comment: "declare only external facts
255
+ # with attribute"; a real AbsentArgument confirmed this before `[]`
256
+ # replaced `delete`). Composite identity is not attempted — `.first`
257
+ # only, no domain in the corpus needs more for a port operation.
258
+ def port_call(verb, aggregate, operation, to:, with:, flat:)
259
+ flat = flat.dup
260
+ identity = operation.identity_attribute(aggregate.hecks_name)
261
+ if to.nil? && identity && flat.key?(identity.name)
262
+ to = flat.delete(identity.name)
263
+ elsif to.nil? && operation.to == aggregate.hecks_name
264
+ identity_name = Array(aggregate.identified_by).first
265
+ to = flat[identity_name] if identity_name && flat.key?(identity_name)
266
+ end
267
+
268
+ target = route(to)
269
+ raise TypeMismatch, "#{operation.hecks_name} requires its receiving aggregate in to:" unless target
270
+
271
+ new(verb: verb, target: target, facts: facts_for(operation, with: with, flat: flat))
272
+ end
273
+
274
+ # BUG#7 — a non-Hash `to:` must be a String, matching Rust's
275
+ # `RoutingEnvelope::from_json` (kernel/routing.rs), which refuses
276
+ # anything neither a JSON string nor object before the domain payload
277
+ # is examined. Surfaced on `examples/roster`'s `Mark`, whose own
278
+ # attribute is literally named `to`: a flat-facts dispatch steals
279
+ # that key into this parameter, and without this check an out-of-range
280
+ # Integer would be accepted as the aggregate identity, leaving Mark's
281
+ # `to` fact absent (AbsentArgument in Ruby, TypeMismatch in Rust). See
282
+ # spec/runtime/routing_envelope_shape_spec.rb.
283
+ def scalar_envelope(to)
284
+ return [to, []] if to.is_a?(String)
285
+
286
+ raise TypeMismatch, "to: must be a string aggregate identity or an entity route, got #{Rendering.describe(to)}"
287
+ end
288
+
289
+ def envelope_hash(to)
290
+ hash = to.transform_keys(&:to_sym)
291
+ unknown = hash.keys - %i[aggregate entity entities]
292
+ raise TypeMismatch, "to: does not recognize #{unknown.sort.join(', ')}" unless unknown.empty?
293
+
294
+ [hash[:aggregate], entity_identities(hash)]
295
+ end
296
+
297
+ # BUG#18 — an entity route naming no entity (`entities: []`, or
298
+ # neither key) refuses here, unconditionally, before entity_depth is
299
+ # consulted: for an aggregate-level command (depth 0), without this,
300
+ # `[].size == 0` would satisfy the depth check and let the degenerate
301
+ # Hash reach the command's own validation. Rust's `RoutingEnvelope::
302
+ # from_json` always refused it at this point. A bare aggregate identity
303
+ # String remains the ordinary aggregate-command shape.
304
+ def entity_identities(hash)
305
+ raise TypeMismatch, "to: takes entity: or entities:, not both" if hash.key?(:entities) && hash.key?(:entity)
306
+
307
+ identities = hash.key?(:entities) ? Array(hash[:entities]) : Array(hash[:entity])
308
+ raise TypeMismatch, "to: entity route requires at least one entity identity" if identities.empty?
309
+
310
+ identities
311
+ end
312
+
313
+ # `with:` is deliberately strict: a caller choosing the explicit
314
+ # envelope cannot smuggle receiver identity back into the payload,
315
+ # and may not mix it with a flat facts hash. Without `with:`
316
+ # (nil or false) the flat facts are the facts, unread —
317
+ # whether `to:` was given never enters this decision (BUG#17).
318
+ def offered_facts(declaring, with:, flat:)
319
+ if with && !flat.empty?
320
+ raise TypeMismatch,
321
+ "dispatch takes command facts in with:, not both with: and a flat facts hash"
322
+ end
323
+
324
+ return flat unless with
325
+ raise TypeMismatch, "with: must be a hash of command facts" unless with.is_a?(Hash)
326
+
327
+ offered = with.transform_keys(&:to_sym)
328
+ declared = declaring.attributes.map { |attribute| attribute.name.to_sym }
329
+ refuse_unknown_facts!(declaring, offered, declared)
330
+ refuse_absent_facts!(declaring, offered, declared)
331
+ offered
332
+ end
333
+
334
+ # Unknown before absent — a `with:` that is both still refuses
335
+ # UnknownArgument first.
336
+ def refuse_unknown_facts!(declaring, offered, declared)
337
+ unknown = (offered.keys - declared).sort
338
+ return if unknown.empty?
339
+
340
+ raise UnknownArgument,
341
+ RefusalWording.render_site("UnknownArgument", "unknown_args",
342
+ command: declaring.hecks_name, unknown: unknown,
343
+ declared: declared)
344
+ end
345
+
346
+ def refuse_absent_facts!(declaring, offered, declared)
347
+ absent = declaring.attributes.reject(&:optional?).map { |attribute| attribute.name.to_sym } - offered.keys
348
+ return if absent.empty?
349
+
350
+ raise AbsentArgument,
351
+ RefusalWording.render_site("AbsentArgument", "absent_args",
352
+ command: declaring.hecks_name, absent: absent,
353
+ declared: declared)
354
+ end
355
+ end
356
+ end
357
+ end
358
+ end
@@ -19,7 +19,7 @@ module Hecks
19
19
  # `Widget::Item.Add(...)` sugar. A caller that only ever dispatches
20
20
  # by FQN string (`SmokeTest`, the one caller so far) can pass
21
21
  # `false` to skip it: `Facade::Surface.install` puts a bare global
22
- # Ruby constant on `Object` per domain AND per aggregate name, with
22
+ # Ruby constant on `Object` per domain and per aggregate name, with
23
23
  # no scoping and no cleanup hook, so a tool booting arbitrary
24
24
  # throwaway domains under generic names ("Widget", "Item", "Tag")
25
25
  # would otherwise leak those names into the rest of the process —
@@ -37,6 +37,20 @@ module Hecks
37
37
  # its own env var name and passes the resulting string straight
38
38
  # through, e.g. `Hecks.boot(path, environment:
39
39
  # ENV.fetch("MYAPP_ENV", "development"))`.
40
+ #
41
+ # @param path [String] a domain directory to boot, resolved through
42
+ # `Ports::Loading#bluebook_directory`
43
+ # @param shared [String, nil] an explicit shared ports/adapters root override; nil
44
+ # resolves it by walking up from the domain directory
45
+ # @param install_facade [Boolean] whether to install the `Widget::Item.Add(...)`
46
+ # facade sugar
47
+ # @param environment [String, nil] the environment overlay to load after the
48
+ # domain's own files (e.g. `"production"`); nil loads none
49
+ # @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted dispatcher
50
+ # @raise [Errno::ENOENT] if `path` names neither a domain directory nor one holding
51
+ # a `bluebook/` subdirectory
52
+ # @raise [Runtime::WiringError] if a boot gate finds a wiring problem (an undeclared
53
+ # bind, a compute/rekey rule with no persistence plugin loaded, …)
40
54
  def self.boot(path, shared: nil, install_facade: true, environment: nil)
41
55
  loading = Ports::Loading.bootstrap
42
56
  directory = loading.bluebook_directory(path)
@@ -52,10 +66,11 @@ module Hecks
52
66
  run_boot_gates!(registry, directory)
53
67
  dispatcher = dispatcher_for(registry)
54
68
  redrive_outbox!(dispatcher)
69
+ seed_privacy_markings!(dispatcher, registry)
55
70
  install_facade ? bind_runtime(dispatcher) : dispatcher
56
71
  end
57
72
 
58
- # THE OUTBOX'S BOOT-TIME RECONCILIATION — after the dispatcher
73
+ # The outbox's boot-time reconciliation — after the dispatcher
59
74
  # exists (a row's consumer runs through its interpreters, so this
60
75
  # cannot be a plain registry gate the way saga rehydration is) and
61
76
  # after saga rehydration (a redriven row may advance a saga, which
@@ -63,13 +78,44 @@ module Hecks
63
78
  # provably never started; `claimed` rows are surfaced, never
64
79
  # auto-redriven — see `Runtime::Outbox`. A remote dispatcher has
65
80
  # no local stores to scan.
81
+ #
82
+ # @param dispatcher [Runtime::Dispatcher, Runtime::RemoteDispatcher] the
83
+ # just-booted dispatcher; a no-op unless it exposes an `outbox`
84
+ # @return [void]
66
85
  def self.redrive_outbox!(dispatcher)
67
86
  return unless dispatcher.respond_to?(:outbox)
68
87
 
69
88
  dispatcher.outbox.redrive!
70
89
  end
71
90
 
72
- # THE EXPLICIT-FILE FORM — `paths` names the exact bluebook/hecksagon/
91
+ # Turns every `.hecksagon`-declared `AggregateDoor#mark_sensitive` fact into a real
92
+ # `Privacy::Marking.Mark` — after the dispatcher exists (dispatch needs a bound
93
+ # repository), idempotent across reboots (a marking already present is never
94
+ # re-dispatched, the same "provably never started" restraint `redrive_outbox!`
95
+ # holds itself to above). A no-op when nothing declared one, or when the domain
96
+ # never attached Privacy at all.
97
+ #
98
+ # @param dispatcher [Runtime::Dispatcher, Runtime::RemoteDispatcher] the just-booted
99
+ # dispatcher `Privacy::Marking.Mark` dispatches through
100
+ # @param registry [Runtime::Registry] the just-booted registry `pending_privacy_markings`
101
+ # was recorded on
102
+ # @return [void]
103
+ def self.seed_privacy_markings!(dispatcher, registry)
104
+ return if registry.pending_privacy_markings.empty?
105
+ return unless registry.bluebook("Privacy")
106
+
107
+ already_marked_by_domain = registry.pending_privacy_markings.map { |marking| marking[:domain] }.uniq.to_h do |domain|
108
+ [domain, dispatcher.query("Privacy::Marking.ForDomain", domain: domain).map { |row| row[:attribute_path][:value] }]
109
+ end
110
+
111
+ registry.pending_privacy_markings.each do |marking|
112
+ next if already_marked_by_domain[marking[:domain]].include?(marking[:attribute_path])
113
+
114
+ dispatcher.dispatch_flat("Privacy::Marking.Mark", marking)
115
+ end
116
+ end
117
+
118
+ # The explicit-file form — `paths` names the exact bluebook/hecksagon/
73
119
  # world files to boot, in place, wherever they actually live. `boot`
74
120
  # above only ever takes a directory and globs it; that is the right
75
121
  # shape for a real deployment (`examples/banking`, a domain someone
@@ -79,18 +125,30 @@ module Hecks
79
125
  # (`Hecks::Behaviors`), but this carries no behaviors-specific
80
126
  # logic and is not gated behind requiring that module.
81
127
  #
82
- # NO COPYING, NO TEMP DIRECTORY. A prior port of this same idea
128
+ # No copying, no temp directory. A prior port of this same idea
83
129
  # (vendored into a downstream consumer, read before writing this)
84
130
  # scoped a per-test boot by copying files into `Dir.mktmpdir` — which
85
131
  # destroys real relative paths, and worse, makes a `persisted_by`
86
- # path resolve against the TEMP copy's root instead of the project's
132
+ # path resolve against the temp copy's root instead of the project's
87
133
  # own (confirmed there: a file adapter kept reading and writing the
88
134
  # same deterministic tmp copy across an entire session, because
89
135
  # `Hecks.boot`'s own `root` is always `File.dirname` of whatever
90
136
  # directory it was handed). `directory` here is `File.dirname` of the
91
- # FIRST real path in `paths` — genuinely on disk, not a copy — so
137
+ # first real path in `paths` — genuinely on disk, not a copy — so
92
138
  # every downstream path (`EraCheck`, `persisted_by`, `shared_root`)
93
139
  # resolves exactly as an ordinary directory boot's would.
140
+ #
141
+ # @param paths [Array<String>, String] the exact bluebook/hecksagon/world file paths
142
+ # to boot, in the order they should load
143
+ # @param shared [String, nil] an explicit shared ports/adapters root override; nil
144
+ # resolves it by walking up from `File.dirname` of the first path
145
+ # @param install_facade [Boolean] whether to install the `Widget::Item.Add(...)`
146
+ # facade sugar
147
+ # @param environment [String, nil] the environment overlay to load after the
148
+ # named files (e.g. `"production"`); nil loads none
149
+ # @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] the booted dispatcher
150
+ # @raise [Runtime::WiringError] if a boot gate finds a wiring problem (an undeclared
151
+ # bind, a compute/rekey rule with no persistence plugin loaded, …)
94
152
  def self.boot_files(paths, shared: nil, install_facade: true, environment: nil)
95
153
  loading = Ports::Loading.bootstrap
96
154
  files = Array(paths).map { |path| File.expand_path(path) }
@@ -109,21 +167,30 @@ module Hecks
109
167
  install_facade ? bind_runtime(dispatcher) : dispatcher
110
168
  end
111
169
 
112
- # ADR 0031 — replaces two previously-hardcoded, unconditional calls
113
- # with a per-boot `BootGates` instance holding exactly the gates THIS
114
- # registry's own bound adapters have a capability for. Ordering is
170
+ # Runs every registered boot gate against `registry`, in order.
171
+ #
172
+ # ADR 0031 — a per-boot `BootGates` instance holds exactly the gates this
173
+ # registry's own bound adapters have a capability for, rather than two
174
+ # hardcoded, unconditional calls. Ordering is
115
175
  # preserved: era-checking (when a persistence plugin contributes one)
116
176
  # still runs before `verify!`, saga rehydration still runs after
117
177
  # (conservative — see `SagaPersistence#rehydrate_sagas!`'s own
118
178
  # comment).
119
179
  #
120
180
  # ADR 0033 — this loader no longer names `EraCheck`, or any other
121
- # era-specific class, at all. Every LOADED persistence plugin
181
+ # era-specific class, at all. Every loaded persistence plugin
122
182
  # (`Ports::Persistence.each_plugin` — nothing here if nothing was
123
183
  # ever `require`d) is asked to contribute its own `:pre_verify`/
124
184
  # `:post_verify` gates generically; `:saga_rehydration` is the one
125
185
  # gate core still registers directly, because ADR 0031 already
126
186
  # proved it's not era-specific.
187
+ #
188
+ # @param registry [Runtime::Registry] the registry mid-boot, with its bluebooks,
189
+ # hecksagons, ports, adapters and worlds already loaded
190
+ # @param directory [String] the domain directory the gates report against in a refusal
191
+ # @return [Runtime::BootGates] the gates instance built and run for this boot
192
+ # @raise [Runtime::WiringError] if `registry.verify!` or any contributed gate finds
193
+ # a wiring problem
127
194
  def self.run_boot_gates!(registry, directory)
128
195
  gates = BootGates.new
129
196
  Ports::Persistence.each_plugin { |plugin| plugin.contribute_boot_gates(registry, gates) }
@@ -144,11 +211,17 @@ module Hecks
144
211
  # `TranslationCompute`/`TranslationRekey` data (`bluebook/
145
212
  # translation.rb`, core, no era-specific class involved), so this
146
213
  # needs nothing plugin-specific to ask "does anything declare a
147
- # compute/rekey rule at all." A LOADED persistence plugin (e.g. the
214
+ # compute/rekey rule at all." A loaded persistence plugin (e.g. the
148
215
  # era plugin's own `:era_compute_rules` gate, registered above) runs
149
216
  # the real, adapter-aware version of this check and refuses by name
150
217
  # ("...is bound to Memory") long before this ever would; this only
151
218
  # fires when nothing did, because nothing was loaded to.
219
+ #
220
+ # @param registry [Runtime::Registry] the registry mid-boot, whose declared
221
+ # `translations` are checked
222
+ # @return [void]
223
+ # @raise [Runtime::WiringError] if any translation declares a `computes`/`rekeys`
224
+ # rule while no persistence plugin is loaded to interpret it
152
225
  def self.check_compute_rules_backstop!(registry)
153
226
  return if Ports::Persistence.plugins_loaded?
154
227
 
@@ -165,7 +238,7 @@ module Hecks
165
238
  end
166
239
 
167
240
  # `RemoteDispatcher` for a domain routed through Lambda,
168
- # `Dispatcher` otherwise — the ONE place this decision gets
241
+ # `Dispatcher` otherwise — the one place this decision gets
169
242
  # made, so everything built on top (`Handle`, `AggregateDoor`,
170
243
  # `Facade::Surface`) never has to know which class it's holding.
171
244
  # `registry.bluebooks.keys.first` is the just-booted domain's own
@@ -173,7 +246,7 @@ module Hecks
173
246
  # any `uses_framework` chapter, `bin/project_rust`'s own header
174
247
  # draws the identical distinction), not a directory basename.
175
248
  #
176
- # `dispatched_by("Lambda")` is its OWN, EXPLICIT verb — NOT
249
+ # `dispatched_by("Lambda")` is its own, explicit verb — not
177
250
  # inferred from `deployed_to("AwsLambda")`'s mere presence. Both
178
251
  # Banking's and Embryonaut's `.world` files already declare
179
252
  # `deployed_to("AwsLambda")` (it only means "a deploy target
@@ -183,19 +256,32 @@ module Hecks
183
256
  # this landed. A domain opts in explicitly, the same way
184
257
  # `persisted_by("PostgresEra")` is never inferred from anything
185
258
  # else either.
259
+ #
260
+ # @param registry [Runtime::Registry] the just-booted registry, whose just-booted
261
+ # domain's `.world` is checked for `dispatched_by("Lambda")`
262
+ # @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] a `RemoteDispatcher`
263
+ # bound to `registry` if the domain declares `dispatched_by("Lambda")`, a plain
264
+ # `Dispatcher` otherwise
186
265
  def self.dispatcher_for(registry)
187
266
  domain = registry.bluebooks.keys.first
188
267
  settings = registry.world(domain)&.for_verb("dispatched_by") || {}
189
268
  return Dispatcher.new(registry) unless settings[:adapter] == "Lambda"
190
269
 
191
- RemoteDispatcher.new(registry, region: settings.fetch(:region, "us-east-1"))
270
+ RemoteDispatcher.new(registry, region: settings.fetch(:region, "us-east-1"), function: settings[:function])
192
271
  end
193
272
 
194
- # THE DOOR IS INSTALLED HERE, NOT STAMPED. This used to write the
195
- # dispatcher onto every aggregate's class (`ruby_class.runtime =`) — the
196
- # class-level global that made two boots in one process share one
197
- # name. The facade's modules close over THIS dispatcher instead, so the
198
- # binding lives in the surface a boot installs, not on anything shared.
273
+ # Installs the facade sugar (`Widget::Item.Add(...)`), closed over
274
+ # `dispatcher`, and hands `dispatcher` back.
275
+ #
276
+ # The door is installed here, not stamped: the facade's modules close
277
+ # over this dispatcher, so the binding lives in the surface a boot
278
+ # installs rather than in a class-level global (`ruby_class.runtime =`
279
+ # on every aggregate's class) that would make two boots in one process
280
+ # share one name.
281
+ #
282
+ # @param dispatcher [Runtime::Dispatcher, Runtime::RemoteDispatcher] the
283
+ # just-booted dispatcher the facade should call into
284
+ # @return [Runtime::Dispatcher, Runtime::RemoteDispatcher] `dispatcher`, unchanged
199
285
  def self.bind_runtime(dispatcher)
200
286
  Facade::Surface.install(dispatcher)
201
287
  dispatcher