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
@@ -2,30 +2,34 @@ require_relative "../projector"
2
2
 
3
3
  module Hecks
4
4
  module Projections
5
- # A DOMAIN'S OWN SHAPE, PROJECTED AS MERMAID DIAGRAMS — the same
5
+ # A domain's own shape, projected as Mermaid diagrams — the same
6
6
  # trick `Projections::Reference`/`DocsProjector` already play for
7
- # prose, one level further: a diagram generated FROM the
7
+ # prose, one level further: a diagram generated from the
8
8
  # declaration can't drift from it the way a hand-drawn one
9
9
  # inevitably does, because there is no second copy to forget to
10
10
  # update.
11
11
  #
12
- # MERMAID, NOT GRAPHVIZ (the two considered) — every diagram type
12
+ # ## Why Mermaid, not Graphviz
13
+ #
14
+ # Mermaid, not Graphviz (the two considered) — every diagram type
13
15
  # below has a Mermaid form purpose-built for exactly what the
14
- # underlying construct already is (a `lifecycle` IS a state
16
+ # underlying construct already is (a `lifecycle` is a state
15
17
  # machine, `has_many`/`belongs_to` already speaks in cardinality,
16
- # `emits`/`trigger` already IS a directed graph), and the output is
18
+ # `emits`/`trigger` already is a directed graph), and the output is
17
19
  # plain text that renders natively wherever this project's own docs
18
20
  # already live — GitHub markdown, this repo's generated docs, Claude
19
21
  # Artifacts — with no build step and no external binary. Graphviz's
20
- # DOT format needs an actual render step (a `dot` binary, or a WASM
22
+ # dot format needs an actual render step (a `dot` binary, or a WASM
21
23
  # port) to become anything viewable, which is a real dependency this
22
24
  # repository's own discipline (see rust/parser's Cargo.toml: "no
23
25
  # dependency earns its way past std") would rather not take just to
24
26
  # draw a diagram.
25
27
  #
26
- # FOUR DIAGRAM KINDS, one file each per domain except lifecycles
28
+ # ## Diagram kinds
29
+ #
30
+ # Four diagram kinds, one file each per domain except lifecycles
27
31
  # (one per lifecycle-bearing construct, since that's how a reader
28
- # actually reaches for it — looking at ONE aggregate's states, not
32
+ # actually reaches for it — looking at one aggregate's states, not
29
33
  # every aggregate's at once):
30
34
  #
31
35
  # <Name>_lifecycle.mmd stateDiagram-v2 one per lifecycle
@@ -47,27 +51,29 @@ module Hecks
47
51
  # <Name>_surface.mmd flowchart one per aggregate/entity that
48
52
  # declares at least one command
49
53
  # or query — everything you can
50
- # DO to it and ASK about it,
51
- # AND what each command WRITES,
54
+ # do to it and ask about it,
55
+ # and what each command writes,
52
56
  # in one place
53
57
  # <Name>_saga.mmd stateDiagram-v2 one per process_manager —
54
58
  # its own states, and what
55
59
  # each transition dispatches
56
60
  # elsewhere in the domain
57
- # frameworks.mmd flowchart every OTHER domain this one
61
+ # frameworks.mmd flowchart every other domain this one
58
62
  # depends on — a shared
59
63
  # framework it `uses_framework`,
60
64
  # or a domain a policy reaches
61
65
  # `across` — the one diagram
62
- # here that looks OUTWARD past
66
+ # here that looks outward past
63
67
  # this domain's own boundary
64
68
  #
65
- # CONSTRUCT NAMES (aggregate/entity/command/event) ARE USED BARE,
66
- # UNSANITIZED, as Mermaid node/entity ids — safe because this
69
+ # ## Node and construct naming
70
+ #
71
+ # Construct names (aggregate/entity/command/event) are used bare,
72
+ # unsanitized, as Mermaid node/entity ids — safe because this
67
73
  # language's own word grammar only ever admits simple CamelCase/
68
74
  # snake_case identifiers there (confirmed: no space or punctuation
69
75
  # appears in any real aggregate/command/event name across the corpus
70
- # this projects from). A `role:` STRING IS FREE TEXT, though — the
76
+ # this projects from). A `role:` string is free text, though — the
71
77
  # real corpus already has "Back office"/"Vault officer"/"Branch
72
78
  # clerk" — so `roles.mmd` is the one diagram here that sanitizes a
73
79
  # name into an id (`role_id`) while keeping the real string as the
@@ -79,6 +85,14 @@ module Hecks
79
85
 
80
86
  module_function
81
87
 
88
+ # Projects one domain's whole shape into every diagram kind it has data
89
+ # for, one Mermaid file per entry.
90
+ #
91
+ # @param bluebook [Bluebook::Chapter] the assembled chapter to project
92
+ # @param options [Hash{Symbol => Object}] projection options; only `:hecksagon`
93
+ # (a `Bluebook::Hecksagon`, or `nil`) is read, and only by `frameworks_diagram`
94
+ # @return [Hash{String => String}] each diagram's filename (such as
95
+ # `"relationships.mmd"`) mapped to its rendered Mermaid source
82
96
  def call(bluebook:, options: {})
83
97
  files = {}
84
98
 
@@ -125,25 +139,46 @@ module Hecks
125
139
 
126
140
  # ── shared ────────────────────────────────────────────────────────
127
141
 
128
- # AN ENTITY CAN CARRY ITS OWN LIFECYCLE, RELATIONSHIP, OR COMMAND
129
- # TOO — its own `lifecycle`/`reference_to`/`command` block,
142
+ # Every aggregate and entity in this domain, flattened into one list —
143
+ # everything a lifecycle/relationship/command diagram walks alike.
144
+ #
145
+ # An entity can carry its own lifecycle, relationship, or command
146
+ # too — its own `lifecycle`/`reference_to`/`command` block,
130
147
  # addressed through its holding aggregate the same way
131
148
  # `DocsProjector` already treats an aggregate and its entities
132
149
  # alike. Walking both here means a domain's entity gaining any of
133
150
  # these needs no change to this file.
151
+ #
152
+ # @param bluebook [Bluebook::Chapter] the chapter to walk
153
+ # @return [Array<Bluebook::Aggregate, Class>] every aggregate, and every entity
154
+ # class (a `Bluebook::Entity` subclass) nested under it, in declaration order
134
155
  def holders(bluebook)
135
156
  bluebook.aggregates.flat_map { |aggregate| [aggregate, *aggregate.entities] }
136
157
  end
137
158
 
159
+ # The holders (see `holders`) that declare their own `lifecycle` block —
160
+ # one diagram file is projected per lifecycle-bearing holder, not one
161
+ # per domain.
162
+ #
163
+ # @param bluebook [Bluebook::Chapter] the chapter to walk
164
+ # @return [Array<Bluebook::Aggregate, Class>] every holder with a non-nil
165
+ # `lifecycle`
138
166
  def holders_with_lifecycle(bluebook) = holders(bluebook).select(&:lifecycle)
139
167
 
140
- # `chapter_name` DRIVES THE RE-RUN HINT ALWAYS — that's the one
168
+ # Renders the two-line `%%` comment banner every generated diagram file
169
+ # opens with, warning against hand edits and naming how to regenerate it.
170
+ #
171
+ # `chapter_name` drives the re-run hint always — that's the one
141
172
  # argument `bin/project_diagrams` actually takes, regardless of
142
173
  # which single aggregate/entity `subject` happens to name. Passing
143
174
  # the wrong one here once already produced a real, committed
144
175
  # `Order_lifecycle.mmd` telling a reader to run
145
176
  # `bin/project_diagrams <domain-path> Order` — a chapter name
146
177
  # Hecks.boot has never heard of.
178
+ #
179
+ # @param chapter_name [String] the domain's chapter name, used in the re-run hint
180
+ # @param subject [String] prose naming what this specific file was generated from
181
+ # @return [String] the two-line `%%`-commented Mermaid banner, newline-terminated
147
182
  def header(chapter_name, subject)
148
183
  <<~HEADER
149
184
  %% GENERATED by bin/project_diagrams from #{subject} — DO NOT EDIT BY HAND.
@@ -153,6 +188,15 @@ module Hecks
153
188
 
154
189
  # ── lifecycle -> stateDiagram-v2 ─────────────────────────────────
155
190
 
191
+ # Renders one holder's own declared `lifecycle` as a Mermaid
192
+ # `stateDiagram-v2` — its default starting state, and one edge per
193
+ # transition, labeled by the command that causes it.
194
+ #
195
+ # @param bluebook [Bluebook::Chapter] the chapter `holder` belongs to, for the
196
+ # file's re-run banner
197
+ # @param holder [Bluebook::Aggregate, Class] the aggregate or entity class (a
198
+ # `Bluebook::Entity` subclass) whose `lifecycle` to draw
199
+ # @return [String] the rendered Mermaid `stateDiagram-v2` source
156
200
  def lifecycle_diagram(bluebook, holder)
157
201
  lifecycle = holder.lifecycle
158
202
  edges = lifecycle.transitions.flat_map do |command_name, transition|
@@ -169,18 +213,26 @@ module Hecks
169
213
 
170
214
  # ── relationships -> erDiagram ───────────────────────────────────
171
215
 
172
- # STANDARD CROW'S-FOOT READING, the same convention every ORM's own
216
+ # Renders the whole domain's declared `has_many`/`has_one`/`belongs_to`/
217
+ # `reference_to` attributes as a Mermaid `erDiagram`, one edge per
218
+ # reference attribute across every holder.
219
+ #
220
+ # Standard crow's-foot reading, the same convention every ORM's own
173
221
  # ERD generator (Rails' erd gem included) already uses:
174
- # `has_many`/`has_one` are read from the OWNING side — one Holder
222
+ # `has_many`/`has_one` are read from the owning side — one Holder
175
223
  # relates to many/one Target. `belongs_to`/`reference_to` are read
176
- # from the TARGET's side instead — one Target can be pointed at by
177
- # MANY Holders — because a bare reference carries no promise about
224
+ # from the target's side instead — one Target can be pointed at by
225
+ # many Holders — because a bare reference carries no promise about
178
226
  # how many holders point back at it; "many" is the honest default
179
227
  # absent a declared uniqueness rule this language doesn't expose.
180
228
  # `optional?` only ever softens the side that can genuinely be
181
229
  # absent (a nilable reference, an empty has_one) — never the
182
230
  # crow's-foot "many" marker, which is a structural fact independent
183
231
  # of any one instance's optionality.
232
+ #
233
+ # @param bluebook [Bluebook::Chapter] the chapter to project
234
+ # @return [String, nil] the rendered Mermaid `erDiagram` source, or `nil` if the
235
+ # domain declares no reference attribute at all
184
236
  def relationship_diagram(bluebook)
185
237
  edges = holders(bluebook).flat_map do |holder|
186
238
  holder.attributes.select(&:reference?).map { |attribute| relationship_edge(holder, attribute) }
@@ -191,6 +243,14 @@ module Hecks
191
243
  "#{header(bluebook.name, subject)}erDiagram\n#{edges.join("\n")}\n"
192
244
  end
193
245
 
246
+ # One erDiagram edge for one holder's own declared reference attribute,
247
+ # in the DSL word's own crow's-foot reading (see `relationship_diagram`).
248
+ #
249
+ # @param holder [Bluebook::Aggregate, Class] the aggregate or entity class (a
250
+ # `Bluebook::Entity` subclass) that declares `attribute`
251
+ # @param attribute [Bluebook::Attribute] a reference attribute (`attribute.reference?`
252
+ # is true); its `relationship` names which DSL word minted it
253
+ # @return [String] the rendered `erDiagram` edge line
194
254
  def relationship_edge(holder, attribute)
195
255
  target = attribute.type.target_name
196
256
  case attribute.relationship
@@ -205,14 +265,18 @@ module Hecks
205
265
 
206
266
  # ── dispatch -> flowchart ─────────────────────────────────────────
207
267
 
208
- # A COMMAND NODE, STADIUM-SHAPED (`(["..."])`); AN EVENT NODE,
209
- # HEXAGONAL (`{{"..."}}`) — one visual vocabulary for "a thing
268
+ # A command node, stadium-shaped (`(["..."])`); an event node,
269
+ # hexagonal (`{{"..."}}`) — one visual vocabulary for "a thing
210
270
  # someone does" versus "a fact that happened", matching the
211
271
  # language's own verb/event distinction. Command ids are qualified
212
272
  # by their owning aggregate (`cmd_Order_Purchase`) since two
213
273
  # aggregates may share a command name; event ids are bare
214
274
  # (`evt_PizzaCreated`) since an event is this domain's own
215
275
  # addressing key, the same way `policy.on_event` reaches it.
276
+ #
277
+ # @param bluebook [Bluebook::Chapter] the chapter to project
278
+ # @return [String, nil] the rendered Mermaid flowchart source, or `nil` if the
279
+ # domain declares no command `emits` and no policy at all
216
280
  def dispatch_diagram(bluebook)
217
281
  lines = []
218
282
 
@@ -231,21 +295,33 @@ module Hecks
231
295
  "#{header(bluebook.name, subject)}flowchart LR\n#{lines.uniq.join("\n")}\n"
232
296
  end
233
297
 
298
+ # One flowchart edge from a command to one event it declares it `emits`.
299
+ #
300
+ # @param holder [Bluebook::Aggregate, Class] the aggregate or entity class (a
301
+ # `Bluebook::Entity` subclass) that declares `command`
302
+ # @param command [Class] the command class (a `Bluebook::Command` subclass) that
303
+ # emits `event`
304
+ # @param event [String] the emitted event's name
305
+ # @return [String] the rendered `-->|emits|` edge line
234
306
  def emits_edge(holder, command, event)
235
307
  %( #{command_node(holder.hecks_name, command.hecks_name)} -->|emits| #{event_node(event)})
236
308
  end
237
309
 
238
- # `on_event` IS SOMETIMES AGGREGATE-QUALIFIED
239
- # (`"Account.AccountFrozen"`) AND SOMETIMES BARE
310
+ # `on_event` is sometimes aggregate-qualified
311
+ # (`"Account.AccountFrozen"`) and sometimes bare
240
312
  # (`"CustomerSuspended"`) in the real corpus — `emits` never is,
241
313
  # so this always matches against the bare tail, the same
242
314
  # normalization a reader has to do by eye today.
243
315
  #
244
- # A TRIGGER CROSSING INTO ANOTHER DOMAIN (`policy.target_domain`)
316
+ # A trigger crossing into another domain (`policy.target_domain`)
245
317
  # still draws — the target command just has no incoming `emits`
246
318
  # edge of its own here, which honestly shows "dispatch continues
247
319
  # elsewhere" rather than silently dropping the edge. The label
248
320
  # names which domain, so that's not a dead end on the page either.
321
+ #
322
+ # @param policy [Bluebook::Policy] the policy to draw as a trigger edge
323
+ # @return [String] the rendered `-->|triggers|` (or `-->|triggers in <domain>|`)
324
+ # edge line
249
325
  def trigger_edge(policy)
250
326
  bare_event = policy.on_event.to_s.split(".").last
251
327
  aggregate_name, command_name = policy.trigger_command.to_s.split(".", 2)
@@ -253,21 +329,39 @@ module Hecks
253
329
  %( #{event_node(bare_event)} -->|#{label}| #{command_node(aggregate_name, command_name)})
254
330
  end
255
331
 
332
+ # Mermaid stadium-shaped node id and label for one command, qualified by
333
+ # its owning aggregate since two aggregates may share a command name.
334
+ #
335
+ # @param aggregate_name [String] the command's owning aggregate/entity name
336
+ # @param command_name [String] the command's own name
337
+ # @return [String] the rendered `cmd_<aggregate>_<command>(["..."])` node
256
338
  def command_node(aggregate_name, command_name)
257
339
  %(cmd_#{aggregate_name}_#{command_name}(["#{aggregate_name}.#{command_name}"]))
258
340
  end
259
341
 
342
+ # Mermaid hexagonal node id and label for one event, addressed bare since
343
+ # an event is this domain's own addressing key.
344
+ #
345
+ # @param event_name [String] the event's own name
346
+ # @return [String] the rendered `evt_<event>{{"..."}}` node
260
347
  def event_node(event_name) = %(evt_#{event_name}{{"#{event_name}"}})
261
348
 
262
349
  # ── roles -> flowchart ────────────────────────────────────────────
263
350
 
264
- # WHO ISSUES WHAT, ACROSS THE WHOLE DOMAIN — data no existing
351
+ # Renders every command's declared `role` as a Mermaid flowchart, one
352
+ # edge per role issuing a command.
353
+ #
354
+ # Who issues what, across the whole domain — data no existing
265
355
  # projection draws at all today (the reference pages' own
266
356
  # `command_entry` only ever prints a command's role as a single
267
357
  # line of prose, never assembled across commands). A command with
268
358
  # no declared `role` draws nothing — there is no fact to state.
269
359
  # Circle-shaped so a role reads as "who" beside `dispatch.mmd`'s
270
360
  # stadium ("what someone does") and hexagon ("what happened").
361
+ #
362
+ # @param bluebook [Bluebook::Chapter] the chapter to project
363
+ # @return [String, nil] the rendered Mermaid flowchart source, or `nil` if no
364
+ # command in the domain declares a `role`
271
365
  def roles_diagram(bluebook)
272
366
  lines = holders(bluebook).flat_map do |holder|
273
367
  holder.commands.select(&:role).map { |command| role_edge(holder, command) }
@@ -278,22 +372,41 @@ module Hecks
278
372
  "#{header(bluebook.name, subject)}flowchart LR\n#{lines.uniq.join("\n")}\n"
279
373
  end
280
374
 
375
+ # One flowchart edge from a command's declared role to the command
376
+ # itself.
377
+ #
378
+ # @param holder [Bluebook::Aggregate, Class] the aggregate or entity class (a
379
+ # `Bluebook::Entity` subclass) that declares `command`
380
+ # @param command [Class] the command class (a `Bluebook::Command` subclass); its
381
+ # `role` is drawn
382
+ # @return [String] the rendered `-->|issues|` edge line
281
383
  def role_edge(holder, command)
282
384
  %( #{role_node(command.role)} -->|issues| #{command_node(holder.hecks_name, command.hecks_name)})
283
385
  end
284
386
 
387
+ # Mermaid circular node id and label for one role, keeping the real
388
+ # free-text role string as the displayed label (see `role_id`).
389
+ #
390
+ # @param role_name [String] the command's declared `role` text
391
+ # @return [String] the rendered `role_<id>((<role>))` node
285
392
  def role_node(role_name) = %(#{role_id(role_name)}((#{role_name})))
286
393
 
287
- # A ROLE NAME IS FREE TEXT ("Back office", "Vault officer") —
394
+ # Sanitizes a free-text role name into a legal Mermaid node id.
395
+ #
396
+ # A role name is free text ("Back office", "Vault officer") —
288
397
  # unlike every other name this file uses as a bare id, this one
289
398
  # has to be sanitized to become a legal Mermaid identifier. The
290
399
  # real string still appears as the node's own label
291
400
  # (`role_node`); only the id is mangled.
401
+ #
402
+ # @param role_name [String] the command's declared `role` text
403
+ # @return [String] `role_` followed by `role_name` with every run of
404
+ # non-alphanumeric characters collapsed to a single underscore
292
405
  def role_id(role_name) = "role_#{role_name.to_s.gsub(/[^A-Za-z0-9]+/, '_')}"
293
406
 
294
407
  # ── ports -> flowchart ───────────────────────────────────────────
295
408
 
296
- # A PORT OPERATION IS A BOUNDARY TRANSLATION, NOT A VERB OR A FACT —
409
+ # A port operation is a boundary translation, not a verb or a fact —
297
410
  # its own reference page says so plainly ("the builder behind it
298
411
  # defines no `given` or `sets`, so an operation cannot read
299
412
  # aggregate state or mutate a record itself"), so it gets a third
@@ -302,21 +415,25 @@ module Hecks
302
415
  # state landing somewhere, the same reason a data store gets one
303
416
  # in an ordinary flowchart.
304
417
  #
305
- # TWO EDGE KINDS PER OPERATION: a dotted "exposes" edge from the
418
+ # Two edge kinds per operation: a dotted "exposes" edge from the
306
419
  # aggregate the port hangs off (always present — a port always
307
420
  # belongs to exactly one aggregate), and a solid "to:" edge to
308
421
  # whichever aggregate the operation itself names as its receiver
309
- # (present only when `to:` is declared — PR #351's own real
310
- # addition; before it, this data didn't exist to draw at all).
422
+ # (present only when `to:` is declared — an operation with no
423
+ # declared target has no destination of its own to draw).
311
424
  # `emits` reuses `dispatch.mmd`'s own `event_node` unchanged — the
312
425
  # same fact, reached from a different direction.
313
426
  #
314
- # `bluebook.aggregates`, NOT the shared `holders` — unlike a
315
- # lifecycle/relationship/command, a port belongs to an AGGREGATE
427
+ # `bluebook.aggregates`, not the shared `holders` — unlike a
428
+ # lifecycle/relationship/command, a port belongs to an aggregate
316
429
  # only; an entity has no `ports` method at all (confirmed: calling
317
430
  # it raises, it isn't just always empty), so walking entities here
318
431
  # the way every other diagram in this file does would crash on
319
432
  # the first entity-bearing domain.
433
+ #
434
+ # @param bluebook [Bluebook::Chapter] the chapter to project
435
+ # @return [String, nil] the rendered Mermaid flowchart source, or `nil` if the
436
+ # domain declares no port operation at all
320
437
  def ports_diagram(bluebook)
321
438
  lines = bluebook.aggregates.flat_map do |holder|
322
439
  holder.ports.flat_map { |port| port.operations.map { |operation| port_edges(holder, port, operation) } }
@@ -328,6 +445,14 @@ module Hecks
328
445
  "#{header(bluebook.name, subject)}flowchart LR\n#{lines.uniq.join("\n")}\n"
329
446
  end
330
447
 
448
+ # All of one port operation's own edges: the aggregate that exposes it,
449
+ # its optional `to:` routing target, and one edge per event it `emits`.
450
+ #
451
+ # @param holder [Bluebook::Aggregate] the aggregate `port` is attached to
452
+ # @param port [Bluebook::DomainPort] the port `operation` belongs to
453
+ # @param operation [Bluebook::PortOperation] the operation to draw
454
+ # @return [Array<String>] the rendered edge lines: one "exposes" edge, plus a
455
+ # "to:" edge when `operation.to` is declared, plus one "emits" edge per event
331
456
  def port_edges(holder, port, operation)
332
457
  op = port_operation_node(holder.hecks_name, port.name, operation.hecks_name)
333
458
  edges = [" #{holder.hecks_name}[(#{holder.hecks_name})] -.->|exposes| #{op}"]
@@ -336,6 +461,13 @@ module Hecks
336
461
  edges
337
462
  end
338
463
 
464
+ # Mermaid trapezoidal node id and label for one port operation, qualified
465
+ # by both its owning aggregate and its owning port.
466
+ #
467
+ # @param aggregate_name [String] the operation's owning aggregate name
468
+ # @param port_name [String] the operation's owning port name
469
+ # @param operation_name [String] the operation's own name
470
+ # @return [String] the rendered `op_<aggregate>_<port>_<operation>[/"..."/]` node
339
471
  def port_operation_node(aggregate_name, port_name, operation_name)
340
472
  id = "op_#{aggregate_name}_#{port_name}_#{operation_name}"
341
473
  %(#{id}[/"#{port_name}.#{operation_name}"/])
@@ -343,17 +475,20 @@ module Hecks
343
475
 
344
476
  # ── read models -> flowchart ─────────────────────────────────────
345
477
 
346
- # THE READ-SIDE COMPLEMENT TO `relationships.mmd` — that diagram
347
- # shows how aggregates reference each other for WRITES
478
+ # Renders the whole domain's declared `read_models` as a Mermaid
479
+ # flowchart, one edge per aggregate a read model is assembled from.
480
+ #
481
+ # The read-side complement to `relationships.mmd` — that diagram
482
+ # shows how aggregates reference each other for writes
348
483
  # (`has_many`/`belongs_to`/`reference_to`); this shows how a
349
- # `read_model` ASSEMBLES data for READS, from
484
+ # `read_model` assembles data for reads, from
350
485
  # `aggregate_heads` — the same list `where`/`group_by`/`order_by`
351
486
  # all operate over, and the one fact every read_model has
352
487
  # regardless of whether it's rooted (`reference_target`) or
353
488
  # gathers heads with no root at all (a rootless read model, real
354
489
  # in the corpus: `AccountsByKind`).
355
490
  #
356
- # A READ MODEL IS A SUBROUTINE SHAPE (`[[...]]`, "a predefined
491
+ # A read model is a subroutine shape (`[[...]]`, "a predefined
357
492
  # process") — a fourth shape, beside `ports.mmd`'s trapezoid and
358
493
  # `dispatch.mmd`'s stadium/hexagon: not a verb, not a fact, not a
359
494
  # boundary translation, but a standing, reusable view. Every
@@ -363,12 +498,16 @@ module Hecks
363
498
  # (real in banking: `Account` feeds four) merges into one node
364
499
  # across the whole diagram.
365
500
  #
366
- # THE LABEL NAMES THE SHAPE OF THE ANSWER, NOT JUST THE NAME —
501
+ # The label names the shape of the answer, not just the name —
367
502
  # `(count)`/`(median: field)` for the two real aggregations in the
368
503
  # corpus, nothing appended for an ordinary row-returning
369
504
  # read_model. Still MVP scope: `where`/`group_by`/`order_by`
370
505
  # aren't drawn at all yet — real facts, not invented, just not
371
506
  # this diagram's job yet.
507
+ #
508
+ # @param bluebook [Bluebook::Chapter] the chapter to project
509
+ # @return [String, nil] the rendered Mermaid flowchart source, or `nil` if the
510
+ # domain declares no `read_model` at all
372
511
  def read_model_diagram(bluebook)
373
512
  lines = bluebook.read_models.flat_map { |read_model| read_model_edges(read_model) }
374
513
  return nil if lines.empty?
@@ -377,16 +516,22 @@ module Hecks
377
516
  "#{header(bluebook.name, subject)}flowchart LR\n#{lines.uniq.join("\n")}\n"
378
517
  end
379
518
 
519
+ # One edge per aggregate one read model is assembled from, into that
520
+ # read model's own subroutine-shaped node.
521
+ #
522
+ # @param read_model [Bluebook::ReadModel] the read model to draw
523
+ # @return [Array<String>] the rendered edge lines, one per entry in
524
+ # `read_model.to_h[:aggregate_heads]`
380
525
  def read_model_edges(read_model)
381
526
  shape = read_model.to_h
382
527
  node = %(rm_#{shape[:name]}[["#{read_model_label(shape)}"]])
383
528
  Array(shape[:aggregate_heads]).map do |head|
384
- # QUOTED, NOT BARE — an edge label containing `[` or `]`
529
+ # **Quoted, not bare** — an edge label containing `[` or `]`
385
530
  # (`accounts[]`, marking the "many" side) breaks Mermaid's own
386
531
  # `|label|` parser outright if left unquoted: it reads the
387
- # `[` as the START OF A NEW NODE SHAPE mid-label, not text.
532
+ # `[` as the start of a new node shape mid-label, not text.
388
533
  # Confirmed live against the real parser before this quoting
389
- # existed — every OTHER edge label in this file happens to be
534
+ # existed — every other edge label in this file happens to be
390
535
  # a bare word or already-quoted string, so this is the one
391
536
  # spot that needed it.
392
537
  label = head[:many] ? "#{head[:as]}[]" : head[:as]
@@ -394,6 +539,13 @@ module Hecks
394
539
  end
395
540
  end
396
541
 
542
+ # The read model's own subroutine node label — its name, with
543
+ # `(count)`/`(median: field)` appended for the two aggregations this
544
+ # diagram states.
545
+ #
546
+ # @param shape [Hash{Symbol => Object}] a `Bluebook::ReadModel#to_h` result;
547
+ # reads `:name`, `:count` and `:median_field`
548
+ # @return [String] the node's display label
397
549
  def read_model_label(shape)
398
550
  return "#{shape[:name]} (count)" if shape[:count]
399
551
  return "#{shape[:name]} (median: #{shape[:median_field]})" if shape[:median_field]
@@ -403,50 +555,59 @@ module Hecks
403
555
 
404
556
  # ── surface -> flowchart ─────────────────────────────────────────
405
557
 
406
- # "WHAT CAN I DO TO THIS, WHAT CAN I ASK ABOUT IT" — one file per
558
+ # Renders one holder's own commands (and what each writes) and queries
559
+ # as a Mermaid flowchart.
560
+ #
561
+ # "What can I do to this, what can I ask about it" — one file per
407
562
  # holder, unlike every other diagram here: `dispatch.mmd` already
408
563
  # shows a command's own onward reaction chain, but never an
409
- # aggregate's own FULL command/query menu in one place, and
564
+ # aggregate's own full command/query menu in one place, and
410
565
  # `roles.mmd` shows who issues a command without saying what else
411
566
  # that same aggregate answers. This is the one diagram meant to
412
567
  # be read starting from the aggregate, not from a verb or a fact.
413
568
  #
414
- # A QUERY IS A DIAMOND — a fifth shape, beside `dispatch.mmd`'s
569
+ # A query is a diamond — a fifth shape, beside `dispatch.mmd`'s
415
570
  # stadium/hexagon, `ports.mmd`'s trapezoid, and `read_models.mmd`'s
416
571
  # subroutine: a question with an answer, not a verb that changes
417
572
  # anything. Command edges are solid ("does"); query edges are
418
573
  # dotted ("asks") — the same solid/dotted split `ports.mmd`
419
574
  # already uses for "routes to:" versus "exposes".
420
575
  #
421
- # A WRITE TARGET IS A PLAIN RECTANGLE — a sixth shape, the first
576
+ # A write target is a plain rectangle — a sixth shape, the first
422
577
  # here with no special bracket at all: an attribute is the
423
578
  # smallest, most passive thing this vocabulary names, a single
424
- # field living INSIDE the cylinder rather than a bounded thing of
579
+ # field living inside the cylinder rather than a bounded thing of
425
580
  # its own. `command.mutations` (`sets`/`increment`/`decrement`/
426
581
  # `append`) was invisible everywhere before this — not just in a
427
- # diagram, in ANY projection, including the prose ones — despite
582
+ # diagram, in any projection, including the prose ones — despite
428
583
  # being the single densest fact in the whole IR (53 real
429
584
  # mutations across pizzas + banking). `dispatch.mmd` draws what a
430
- # command EMITS; this draws what it WRITES, the other half of
585
+ # command emits; this draws what it writes, the other half of
431
586
  # "what actually happens" a command never showed before.
432
587
  #
433
- # THE SAME ATTRIBUTE NODE MERGES ACROSS COMMANDS — real in
588
+ # The same attribute node merges across commands — real in
434
589
  # banking: `Account.Credit` and `Account.Debit` both point at the
435
590
  # same `balance` node, the same "one node, several incoming
436
591
  # edges" merge `read_models.mmd` already does for an aggregate
437
592
  # fed by several read_models.
438
593
  #
439
- # THE LABEL NAMES THE REAL SOURCE, NOT JUST THE VERB — an
594
+ # The label names the real source, not just the verb — an
440
595
  # increment/decrement/set almost always takes its value from an
441
- # argument, but not always the SAME-NAMED one: real in banking,
596
+ # argument, but not always the same-named one: real in banking,
442
597
  # `Account.Credit`'s own `balance` is incremented by its
443
598
  # `amount` argument, and `LedgerEntry.Amend`'s own `amount` is
444
599
  # incremented by its `adjustment` argument. A literal source
445
600
  # (pizzas' own `Order.Purchase` sets `status` to the literal
446
601
  # `"sold"`, not an argument at all) is named as verbatim as
447
602
  # every other fact in this file. `append`'s own fields carry no
448
- # single source at all — its own field NAMES are the fact worth
603
+ # single source at all — its own field names are the fact worth
449
604
  # stating (real: `Order.AddTopping` appends `name, amount`).
605
+ #
606
+ # @param bluebook [Bluebook::Chapter] the chapter `holder` belongs to, for the
607
+ # file's re-run banner
608
+ # @param holder [Bluebook::Aggregate, Class] the aggregate or entity class (a
609
+ # `Bluebook::Entity` subclass) whose commands and queries to draw
610
+ # @return [String] the rendered Mermaid flowchart source
450
611
  def surface_diagram(bluebook, holder)
451
612
  lines = holder.commands.map do |command|
452
613
  " #{holder.hecks_name}[(#{holder.hecks_name})] -->|does| #{command_node(holder.hecks_name, command.hecks_name)}"
@@ -464,10 +625,25 @@ module Hecks
464
625
  "#{header(bluebook.name, subject)}flowchart LR\n#{lines.uniq.join("\n")}\n"
465
626
  end
466
627
 
628
+ # Mermaid diamond-shaped node id and label for one query, qualified by
629
+ # its owning aggregate.
630
+ #
631
+ # @param aggregate_name [String] the query's owning aggregate/entity name
632
+ # @param query_name [String] the query's own name
633
+ # @return [String] the rendered `qry_<aggregate>_<query>{"..."}` node
467
634
  def query_node(aggregate_name, query_name)
468
635
  %(qry_#{aggregate_name}_#{query_name}{"#{aggregate_name}.#{query_name}"})
469
636
  end
470
637
 
638
+ # One flowchart edge from a command to one attribute it mutates,
639
+ # labeled with what the mutation writes (see `mutation_label`).
640
+ #
641
+ # @param holder [Bluebook::Aggregate, Class] the aggregate or entity class (a
642
+ # `Bluebook::Entity` subclass) that declares `command` and the mutated attribute
643
+ # @param command [Class] the command class (a `Bluebook::Command` subclass) that
644
+ # declares `mutation`
645
+ # @param mutation [Bluebook::Mutation] the mutation to draw
646
+ # @return [String] the rendered `-->|"..."|` edge line
471
647
  def mutation_edge(holder, command, mutation)
472
648
  shape = mutation.to_h
473
649
  label = mutation_label(shape)
@@ -475,11 +651,18 @@ module Hecks
475
651
  %( #{command_node(holder.hecks_name, command.hecks_name)} -->|"#{label}"| #{target})
476
652
  end
477
653
 
654
+ # The edge label naming what one mutation writes — the op, and either
655
+ # its bound field names (an append/delegate/corrects) or its value
656
+ # source (see `mutation_source_detail`).
657
+ #
658
+ # @param shape [Hash{Symbol => Object}] a `Bluebook::Mutation#to_h` result;
659
+ # reads `:op`, and either `:fields` or `:source`
660
+ # @return [String] the rendered `"<op>s: <detail>"` label text
478
661
  def mutation_label(shape)
479
662
  verb = "#{shape[:op]}s"
480
- # `fields:` (not `source:`) IS the multi-binding shape
663
+ # `fields:` (not `source:`) is the multi-binding shape
481
664
  # (`Mutation#to_h`'s own `[:append, :delegate, :corrects]`
482
- # branch) — checked by the KEY'S PRESENCE, not by re-listing
665
+ # branch) — checked by the key's presence, not by re-listing
483
666
  # which ops use it a second time here, the same lesson
484
667
  # `Change.op`'s own `admits: Vocabulary::MutationOp` already
485
668
  # drew (command.bluebook's own comment): a second list of "the
@@ -492,7 +675,7 @@ module Hecks
492
675
  "#{verb}: #{detail}"
493
676
  end
494
677
 
495
- # A LITERAL VALUE CAN CONTAIN A DOUBLE QUOTE OF ITS OWN — real in
678
+ # A literal value can contain a double quote of its own — real in
496
679
  # banking: `Customer.Reinstate` sets `standing` to a rendered
497
680
  # value-object literal, `{:value=>"good"}`, whose own embedded `"`
498
681
  # broke this label's outer `|"..."|` quoting outright (caught by
@@ -501,33 +684,46 @@ module Hecks
501
684
  # was caught). Swapped for a single quote here rather than
502
685
  # escaped, the same "state it, don't invent it, just make it
503
686
  # legal Mermaid" trade `read_models.mmd`'s own quoting fix made.
687
+ #
688
+ # @param source [Hash{Symbol => Object}] a classified mutation source: `{kind:
689
+ # "literal", value:}`, `{kind: "argument", name:}`, or `{kind: "state", name:}`
690
+ # @return [String] `source[:value]` quoted for a literal, `source[:name]` for an
691
+ # argument, or `source[:kind]` itself for any other kind
504
692
  def mutation_source_detail(source)
505
693
  case source[:kind]
506
694
  when "literal" then "'#{source[:value].to_s.tr('"', "'")}'"
507
695
  when "argument" then source[:name]
508
- else source[:kind] # a source kind this file has no real corpus example of yet — named, not hidden
696
+ else
697
+ # A source kind this file has no real corpus example of yet — named, not hidden.
698
+ source[:kind]
509
699
  end
510
700
  end
511
701
 
702
+ # Mermaid plain-rectangle node id and label for one attribute a
703
+ # mutation writes, qualified by its owning holder.
704
+ #
705
+ # @param holder_name [String] the attribute's owning aggregate/entity name
706
+ # @param attribute_name [Symbol, String] the mutated attribute's own name
707
+ # @return [String] the rendered `attr_<holder>_<attribute>[<attribute>]` node
512
708
  def attribute_node(holder_name, attribute_name)
513
709
  %(attr_#{holder_name}_#{attribute_name}[#{attribute_name}])
514
710
  end
515
711
 
516
712
  # ── sagas -> stateDiagram-v2 ─────────────────────────────────────
517
713
 
518
- # A SAGA HAS A LIFECYCLE TOO — the same `stateDiagram-v2` shape
714
+ # A saga has a lifecycle too — the same `stateDiagram-v2` shape
519
715
  # `lifecycle_diagram` already draws, one file per process_manager
520
716
  # the same way lifecycle is one file per lifecycle-bearing holder.
521
717
  # What's different is the label: a lifecycle's own edge is labeled
522
- # by the COMMAND that causes it (an aggregate transitions because
523
- # something was DONE to it); a saga's edge is labeled by the EVENT
524
- # that causes it (a saga advances because something HAPPENED,
718
+ # by the command that causes it (an aggregate transitions because
719
+ # something was done to it); a saga's edge is labeled by the event
720
+ # that causes it (a saga advances because something happened,
525
721
  # possibly nowhere near the saga itself) — the same command/event
526
722
  # split `dispatch.mmd`'s own stadium/hexagon vocabulary already
527
723
  # draws, here spent on which noun labels a stateDiagram-v2 edge
528
724
  # instead.
529
725
  #
530
- # THE LABEL ALSO NAMES WHAT THE TRANSITION DISPATCHES — a fact no
726
+ # The label also names what the transition dispatches — a fact no
531
727
  # existing diagram states for a saga at all: a lifecycle's own
532
728
  # edge only ever names the one command that caused it; a saga's
533
729
  # edge can fire several commands at once (real in banking:
@@ -538,12 +734,18 @@ module Hecks
538
734
  # fires lands inside its own bluebook chapter, so this never needs
539
735
  # `dispatch.mmd`'s own "triggers in X" cross-domain label.
540
736
  #
541
- # THE COMPENSATING LEG READS LIKE ANY OTHER — its own trigger is
737
+ # The compensating leg reads like any other — its own trigger is
542
738
  # the literal string "refused" (`ProcessManager::REFUSED`, this
543
739
  # language's own Trigger vocabulary), not invented text: a
544
740
  # dispatch declined is exactly as real a cause of a state
545
741
  # transition as an event announced, and the diagram states it
546
742
  # exactly as verbatim as every other edge here does.
743
+ #
744
+ # @param bluebook [Bluebook::Chapter] the chapter `saga` belongs to, for the
745
+ # file's re-run banner
746
+ # @param saga [Bluebook::ProcessManager] the process manager whose states and
747
+ # dispatches to draw
748
+ # @return [String] the rendered Mermaid `stateDiagram-v2` source
547
749
  def saga_diagram(bluebook, saga)
548
750
  edges = saga.handlers.map { |handler| saga_edge(handler, saga) }
549
751
 
@@ -556,20 +758,29 @@ module Hecks
556
758
  MERMAID
557
759
  end
558
760
 
559
- # THE REFUSED EDGE'S OWN DISPATCH LIST IS PARTLY DERIVED NOW —
761
+ # One stateDiagram-v2 edge for one saga handler, labeled by the event
762
+ # that causes it and what it dispatches.
763
+ #
764
+ # The `REFUSED` edge's own dispatch list is partly derived now —
560
765
  # per-dispatch saga compensation (`compensates`) moved a saga's own
561
- # compensating dispatches OFF the hand-written `on :refused` leg
766
+ # compensating dispatches off the hand-written `on :refused` leg
562
767
  # and onto whichever forward dispatch each one undoes, so
563
- # `handler.dispatches` alone would render an EMPTY compensating
564
- # edge for any saga using it — accurate to the DECLARATION, wrong
768
+ # `handler.dispatches` alone would render an empty compensating
769
+ # edge for any saga using it — accurate to the declaration, wrong
565
770
  # about what the runtime actually does at refusal (it derives and
566
771
  # fires every declared `compensates`, newest first). `saga` is
567
- # passed through for exactly this — only the REFUSED handler needs
772
+ # passed through for exactly this — only the `REFUSED` handler needs
568
773
  # it, every other edge's own `handler.dispatches` already says
569
774
  # everything real about it.
775
+ #
776
+ # @param handler [Bluebook::ProcessManagerHandler] the handler row to draw as
777
+ # an edge
778
+ # @param saga [Bluebook::ProcessManager] the process manager `handler` belongs
779
+ # to, needed only to derive the `REFUSED` edge's compensating dispatches
780
+ # @return [String] the rendered `-->` stateDiagram-v2 edge line
570
781
  def saga_edge(handler, saga)
571
782
  label = handler.event_type
572
- # DERIVED FIRST, then the hand-written body — the same order
783
+ # Derived first, then the hand-written body — the same order
573
784
  # `SagaInterpreter#unwind` actually runs them in (every
574
785
  # completed leg's own `compensates` before this leg's own
575
786
  # hand-written dispatches), not declaration order on the page.
@@ -585,33 +796,41 @@ module Hecks
585
796
  # derives and fires (newest-first, at actual refusal time; this
586
797
  # diagram states them in declaration order, since it draws the
587
798
  # saga's own shape, not one instance's own runtime history).
799
+ #
800
+ # @param saga [Bluebook::ProcessManager] the process manager whose declared
801
+ # compensations to list
802
+ # @return [Array<String>] the name of each compensating command declared anywhere
803
+ # in `saga`, in declaration order
588
804
  def derived_compensations(saga)
589
805
  saga.handlers.flat_map { |handler| handler.dispatches.filter_map { |dispatch| dispatch.compensates&.command_name } }
590
806
  end
591
807
 
592
808
  # ── frameworks -> flowchart ─────────────────────────────────────
593
809
 
594
- # EVERY OTHER DIAGRAM IN THIS FILE STAYS INSIDE ONE DOMAIN'S OWN
595
- # BOUNDARY — this is the one that steps outside it. A real domain
810
+ # Renders the whole domain's declared `uses_framework` attachments and
811
+ # cross-domain policy targets as a Mermaid flowchart.
812
+ #
813
+ # Every other diagram in this file stays inside one domain's own
814
+ # boundary — this is the one that steps outside it. A real domain
596
815
  # depends on another domain's own aggregates in exactly two ways:
597
816
  # `uses_framework "X"` in its `.hecksagon` (`Hecksagon#framework_
598
- # members`), which loads X's whole bluebook into THIS registry,
817
+ # members`), which loads X's whole bluebook into this registry,
599
818
  # unconditionally, the moment this domain boots; or a policy's own
600
819
  # `across "X"` (`Policy#target_domain`), which only reaches X when
601
820
  # the policy's declared event actually fires. Same underlying
602
821
  # fact `dispatch.mmd`'s own `trigger_edge` already draws from the
603
822
  # command's side ("triggers in X") — this draws it again from the
604
- # DOMAIN's side, next to the structural `uses_framework` fact
823
+ # domain's side, next to the structural `uses_framework` fact
605
824
  # `dispatch.mmd` never sees at all (that lives in the `.hecksagon`,
606
825
  # which no other diagram here is handed).
607
826
  #
608
- # NEITHER THIS DOMAIN NOR EACH DEPENDENCY GETS THE holders() TREATMENT
609
- # — a whole domain is drawn as ONE cylinder, the same "a bounded,
827
+ # Neither this domain nor each dependency gets the holders() treatment
828
+ # — a whole domain is drawn as one cylinder, the same "a bounded,
610
829
  # addressable thing" shape every other diagram here already spends
611
830
  # on a single aggregate, just scaled up one level: a domain is a
612
831
  # bigger box the same kind of box lives inside.
613
832
  #
614
- # DOTTED FOR `attaches`, SOLID FOR `reaches across` — the reverse
833
+ # Dotted for `attaches`, solid for `reaches across` — the reverse
615
834
  # of which fact is "always true" between the two: attaching a
616
835
  # framework is a standing declaration, true every time this domain
617
836
  # boots, so it gets the same dotted "this always belongs" treatment
@@ -621,14 +840,21 @@ module Hecks
621
840
  # draws for the identical fact, kept solid here so the same
622
841
  # relationship reads the same way in both diagrams.
623
842
  #
624
- # `options[:hecksagon]` IS THE ONE DIAGRAM IN THIS FILE THAT NEEDS
625
- # MORE THAN `bluebook` — `framework_members` lives on the
843
+ # `options[:hecksagon]` is the one diagram in this file that needs
844
+ # more than `bluebook` — `framework_members` lives on the
626
845
  # `Hecksagon`, a sibling IR object `bin/project_diagrams` already
627
846
  # has in hand (`registry.hecksagon(chapter_name)`) but `bluebook`
628
847
  # itself carries no reference to. No hecksagon handed in (an older
629
848
  # caller, or a spec that doesn't care) just means no frameworks.mmd
630
849
  # — same "nothing to state" skip every other diagram here already
631
850
  # takes when its own underlying data is empty.
851
+ #
852
+ # @param bluebook [Bluebook::Chapter] the chapter to project
853
+ # @param hecksagon [Bluebook::Hecksagon, nil] the chapter's `.hecksagon`, holding
854
+ # its `framework_members`, or `nil` if the caller has none to hand in
855
+ # @return [String, nil] the rendered Mermaid flowchart source, or `nil` if
856
+ # `hecksagon` is `nil`, or the domain declares no `uses_framework` and no
857
+ # cross-domain policy target
632
858
  def frameworks_diagram(bluebook, hecksagon)
633
859
  return nil unless hecksagon
634
860
 
@@ -641,6 +867,16 @@ module Hecks
641
867
  "#{header(bluebook.name, subject)}flowchart LR\n#{lines.uniq.join("\n")}\n"
642
868
  end
643
869
 
870
+ # One flowchart edge from this domain to a dependency, dotted for a
871
+ # standing `attaches` and solid for a `reaches across` that only fires
872
+ # when a policy does.
873
+ #
874
+ # @param from [String] this domain's own name
875
+ # @param label [String] the edge label, `"attaches"` or `"reaches across"`
876
+ # @param to [String, Symbol] the dependency domain's name
877
+ # @param dotted [Boolean] whether to render a dotted (`true`) or solid (`false`)
878
+ # arrow
879
+ # @return [String] the rendered edge line
644
880
  def domain_edge(from, label, to, dotted:)
645
881
  arrow = dotted ? "-.->" : "-->"
646
882
  %( #{from}[(#{from})] #{arrow}|#{label}| #{to}[(#{to})])