hecks 0.2.0 → 1.0.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 (419) hide show
  1. checksums.yaml +5 -5
  2. data/lib/hecks/adapters/driven/claude_code.adapter +3 -0
  3. data/lib/hecks/adapters/driven/claude_code.rb +127 -0
  4. data/lib/hecks/adapters/driven/d1.adapter +12 -0
  5. data/lib/hecks/adapters/driven/d1.rb +427 -0
  6. data/lib/hecks/adapters/driven/folder.adapter +3 -0
  7. data/lib/hecks/adapters/driven/folder.rb +199 -0
  8. data/lib/hecks/adapters/driven/google_authentication.adapter +3 -0
  9. data/lib/hecks/adapters/driven/google_authentication.rb +100 -0
  10. data/lib/hecks/adapters/driven/governance_authorization.adapter +3 -0
  11. data/lib/hecks/adapters/driven/governance_authorization.rb +101 -0
  12. data/lib/hecks/adapters/driven/heki/journal.rb +56 -0
  13. data/lib/hecks/adapters/driven/heki/saga_store.rb +99 -0
  14. data/lib/hecks/adapters/driven/heki/snapshot.rb +65 -0
  15. data/lib/hecks/adapters/driven/heki.adapter +4 -0
  16. data/lib/hecks/adapters/driven/heki.rb +179 -0
  17. data/lib/hecks/adapters/driven/identity_registry.adapter +3 -0
  18. data/lib/hecks/adapters/driven/identity_registry.rb +26 -0
  19. data/lib/hecks/adapters/driven/in_memory_ordering.rb +51 -0
  20. data/lib/hecks/adapters/driven/lambda/client.rb +63 -0
  21. data/lib/hecks/adapters/driven/lambda.adapter +4 -0
  22. data/lib/hecks/adapters/driven/lambda.rb +145 -0
  23. data/lib/hecks/adapters/driven/memory.adapter +3 -0
  24. data/lib/hecks/adapters/driven/memory.rb +103 -0
  25. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +3 -0
  26. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +28 -0
  27. data/lib/hecks/adapters/driven/postgres/codec.rb +88 -0
  28. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +207 -0
  29. data/lib/hecks/adapters/driven/postgres.adapter +5 -0
  30. data/lib/hecks/adapters/driven/postgres.rb +438 -0
  31. data/lib/hecks/adapters/driven/postgres_era.adapter +17 -0
  32. data/lib/hecks/adapters/driven/prism.adapter +3 -0
  33. data/lib/hecks/adapters/driven/prism.rb +80 -0
  34. data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
  35. data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
  36. data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
  37. data/lib/hecks/adapters/driven/sqlite/codec.rb +80 -0
  38. data/lib/hecks/adapters/driven/sqlite/projection.rb +172 -0
  39. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +192 -0
  40. data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
  41. data/lib/hecks/adapters/driven/sqlite.rb +306 -0
  42. data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
  43. data/lib/hecks/adapters/driven/system_clock.rb +14 -0
  44. data/lib/hecks/adapters/driven.rb +54 -0
  45. data/lib/hecks/adapters.rb +6 -0
  46. data/lib/hecks/behaviors/dsl.rb +125 -0
  47. data/lib/hecks/behaviors/expectations.rb +340 -0
  48. data/lib/hecks/behaviors/ir.rb +31 -0
  49. data/lib/hecks/behaviors/rspec.rb +42 -0
  50. data/lib/hecks/behaviors/runner.rb +96 -0
  51. data/lib/hecks/behaviors.rb +25 -0
  52. data/lib/hecks/bluebook/aggregate.rb +108 -0
  53. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
  54. data/lib/hecks/bluebook/assembly/build.rb +48 -0
  55. data/lib/hecks/bluebook/assembly/contract.rb +112 -0
  56. data/lib/hecks/bluebook/assembly/contracts.rb +438 -0
  57. data/lib/hecks/bluebook/assembly/marks.rb +228 -0
  58. data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
  59. data/lib/hecks/bluebook/assembly.rb +91 -0
  60. data/lib/hecks/bluebook/attribute.rb +96 -0
  61. data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
  62. data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
  63. data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
  64. data/lib/hecks/bluebook/behaviour/command.rb +116 -0
  65. data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
  66. data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
  67. data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
  68. data/lib/hecks/bluebook/behaviour/lifecycle.rb +68 -0
  69. data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
  70. data/lib/hecks/bluebook/behaviour/process_manager.rb +51 -0
  71. data/lib/hecks/bluebook/behaviour/query.rb +10 -0
  72. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
  73. data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
  74. data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
  75. data/lib/hecks/bluebook/chapter.rb +78 -0
  76. data/lib/hecks/bluebook/command.rb +124 -0
  77. data/lib/hecks/bluebook/domain_port.rb +102 -0
  78. data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
  79. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +1018 -0
  80. data/lib/hecks/bluebook/dsl/attribute_collector.rb +348 -0
  81. data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
  82. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1087 -0
  83. data/lib/hecks/bluebook/dsl/command_builder.rb +714 -0
  84. data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
  85. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
  86. data/lib/hecks/bluebook/dsl/entity_builder.rb +430 -0
  87. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
  88. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
  89. data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
  90. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
  91. data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
  92. data/lib/hecks/bluebook/dsl/policy_builder.rb +135 -0
  93. data/lib/hecks/bluebook/dsl/port_builder.rb +39 -0
  94. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
  95. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +306 -0
  96. data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
  97. data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
  98. data/lib/hecks/bluebook/dsl/rule_reference.rb +173 -0
  99. data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
  100. data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
  101. data/lib/hecks/bluebook/dsl/word_gate.rb +228 -0
  102. data/lib/hecks/bluebook/dsl/world_builder.rb +117 -0
  103. data/lib/hecks/bluebook/dsl.rb +45 -0
  104. data/lib/hecks/bluebook/entity.rb +103 -0
  105. data/lib/hecks/bluebook/expression/canonical_form.rb +123 -0
  106. data/lib/hecks/bluebook/expression/evaluator.rb +304 -0
  107. data/lib/hecks/bluebook/expression/projection.json +218 -0
  108. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
  109. data/lib/hecks/bluebook/expression/resolver.rb +781 -0
  110. data/lib/hecks/bluebook/expression.rb +13 -0
  111. data/lib/hecks/bluebook/hexagon.rb +60 -0
  112. data/lib/hecks/bluebook/lifecycle.rb +42 -0
  113. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
  114. data/lib/hecks/bluebook/meta_validator/judge.rb +621 -0
  115. data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
  116. data/lib/hecks/bluebook/meta_validator/port_judge.rb +51 -0
  117. data/lib/hecks/bluebook/meta_validator/readings.rb +360 -0
  118. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +351 -0
  119. data/lib/hecks/bluebook/meta_validator/shapes.rb +266 -0
  120. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
  121. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
  122. data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
  123. data/lib/hecks/bluebook/meta_validator.rb +542 -0
  124. data/lib/hecks/bluebook/model_check.rb +445 -0
  125. data/lib/hecks/bluebook/pattern_subset.rb +184 -0
  126. data/lib/hecks/bluebook/policy.rb +41 -0
  127. data/lib/hecks/bluebook/process_manager.rb +128 -0
  128. data/lib/hecks/bluebook/project_discovery.rb +30 -0
  129. data/lib/hecks/bluebook/project_loader.rb +40 -0
  130. data/lib/hecks/bluebook/project_register.rb +82 -0
  131. data/lib/hecks/bluebook/query.rb +61 -0
  132. data/lib/hecks/bluebook/read_model.rb +109 -0
  133. data/lib/hecks/bluebook/reference.rb +74 -0
  134. data/lib/hecks/bluebook/smoke_test.rb +166 -0
  135. data/lib/hecks/bluebook/synthesizer.rb +95 -0
  136. data/lib/hecks/bluebook/translation.rb +92 -0
  137. data/lib/hecks/bluebook/value_object.rb +66 -0
  138. data/lib/hecks/bluebook.rb +74 -0
  139. data/lib/hecks/codemod.rb +342 -0
  140. data/lib/hecks/construct.rb +71 -0
  141. data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
  142. data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
  143. data/lib/hecks/deploy/oidc.json +18 -0
  144. data/lib/hecks/doc/reference.rb +410 -0
  145. data/lib/hecks/embryonaut_bluebook.rb +75 -0
  146. data/lib/hecks/facade/cli_door.rb +119 -0
  147. data/lib/hecks/facade/cli_runner.rb +190 -0
  148. data/lib/hecks/facade/command_request.rb +105 -0
  149. data/lib/hecks/facade/handle.rb +173 -0
  150. data/lib/hecks/facade/json_door.rb +166 -0
  151. data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
  152. data/lib/hecks/facade/surface/chapter.rb +107 -0
  153. data/lib/hecks/facade/surface.rb +48 -0
  154. data/lib/hecks/facade.rb +44 -0
  155. data/lib/hecks/forms/app.rb +341 -0
  156. data/lib/hecks/forms/command_form_renderer.rb +113 -0
  157. data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
  158. data/lib/hecks/forms/field_renderer.rb +177 -0
  159. data/lib/hecks/forms/field_shape.rb +232 -0
  160. data/lib/hecks/forms/html.rb +84 -0
  161. data/lib/hecks/forms/index_renderer.rb +35 -0
  162. data/lib/hecks/forms/page.rb +157 -0
  163. data/lib/hecks/forms/params.rb +161 -0
  164. data/lib/hecks/forms/port_argument.rb +46 -0
  165. data/lib/hecks/forms/query_form_renderer.rb +114 -0
  166. data/lib/hecks/forms/record_renderer.rb +119 -0
  167. data/lib/hecks/forms/record_table.rb +68 -0
  168. data/lib/hecks/forms/reference_options.rb +30 -0
  169. data/lib/hecks/forms/value_object_shape.rb +46 -0
  170. data/lib/hecks/forms.rb +54 -0
  171. data/lib/hecks/fqn.rb +94 -0
  172. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
  173. data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
  174. data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
  175. data/lib/hecks/framework/bluebook/governance.bluebook +145 -0
  176. data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
  177. data/lib/hecks/framework/oidc.json +39 -0
  178. data/lib/hecks/framework.rb +90 -0
  179. data/lib/hecks/freezer.rb +67 -0
  180. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +527 -0
  181. data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
  182. data/lib/hecks/fuzzing/isolated_boot.rb +286 -0
  183. data/lib/hecks/fuzzing/properties.rb +1192 -0
  184. data/lib/hecks/fuzzing/replay.rb +668 -0
  185. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
  186. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +80 -0
  187. data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
  188. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +250 -0
  189. data/lib/hecks/fuzzing/sequence_generator.rb +131 -0
  190. data/lib/hecks/fuzzing/value_generator.rb +200 -0
  191. data/lib/hecks/fuzzing.rb +12 -0
  192. data/lib/hecks/grammar/evolve.rb +324 -0
  193. data/lib/hecks/grammar/expression.bluebook +420 -0
  194. data/lib/hecks/grammar/expression_operators.json +1648 -0
  195. data/lib/hecks/grammar/grammar.hecksagon +20 -0
  196. data/lib/hecks/grammar/oidc.json +69 -0
  197. data/lib/hecks/grammar/translation.bluebook +203 -0
  198. data/lib/hecks/grammar.rb +178 -0
  199. data/lib/hecks/ir.rb +126 -0
  200. data/lib/hecks/language/adapter.bluebook +116 -0
  201. data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
  202. data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
  203. data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
  204. data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
  205. data/lib/hecks/language/bluebook/command.bluebook +471 -0
  206. data/lib/hecks/language/bluebook/entity.bluebook +392 -0
  207. data/lib/hecks/language/bluebook/policy.bluebook +189 -0
  208. data/lib/hecks/language/bluebook/process_manager.bluebook +381 -0
  209. data/lib/hecks/language/bluebook/projection.bluebook +267 -0
  210. data/lib/hecks/language/bluebook/query.bluebook +245 -0
  211. data/lib/hecks/language/bluebook/shape.bluebook +292 -0
  212. data/lib/hecks/language/bluebook/syntax.bluebook +448 -0
  213. data/lib/hecks/language/bluebook/vocabulary.bluebook +379 -0
  214. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
  215. data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
  216. data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
  217. data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
  218. data/lib/hecks/language/oidc.json +333 -0
  219. data/lib/hecks/language/port.bluebook +120 -0
  220. data/lib/hecks/language/translation/translation.bluebook +110 -0
  221. data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
  222. data/lib/hecks/language/world/wiring.bluebook +62 -0
  223. data/lib/hecks/language/world/world.bluebook +84 -0
  224. data/lib/hecks/literal.rb +125 -0
  225. data/lib/hecks/naming.rb +174 -0
  226. data/lib/hecks/ports/access_control.port +9 -0
  227. data/lib/hecks/ports/access_control.rb +62 -0
  228. data/lib/hecks/ports/agent/answers.rb +104 -0
  229. data/lib/hecks/ports/agent.port +8 -0
  230. data/lib/hecks/ports/agent.rb +167 -0
  231. data/lib/hecks/ports/authentication.port +6 -0
  232. data/lib/hecks/ports/authentication.rb +50 -0
  233. data/lib/hecks/ports/authorization.port +7 -0
  234. data/lib/hecks/ports/authorization.rb +62 -0
  235. data/lib/hecks/ports/clock.port +5 -0
  236. data/lib/hecks/ports/clock.rb +62 -0
  237. data/lib/hecks/ports/extraction.port +5 -0
  238. data/lib/hecks/ports/extraction.rb +37 -0
  239. data/lib/hecks/ports/identity_assignment.port +5 -0
  240. data/lib/hecks/ports/identity_assignment.rb +45 -0
  241. data/lib/hecks/ports/identity_generation.port +5 -0
  242. data/lib/hecks/ports/identity_generation.rb +49 -0
  243. data/lib/hecks/ports/identity_resolution.port +5 -0
  244. data/lib/hecks/ports/identity_resolution.rb +40 -0
  245. data/lib/hecks/ports/loading.port +4 -0
  246. data/lib/hecks/ports/loading.rb +13 -0
  247. data/lib/hecks/ports/persistence/append_only.rb +138 -0
  248. data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
  249. data/lib/hecks/ports/persistence/execution.rb +23 -0
  250. data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
  251. data/lib/hecks/ports/persistence/plugin.rb +54 -0
  252. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +169 -0
  253. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +128 -0
  254. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +161 -0
  255. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +61 -0
  256. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +304 -0
  257. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +171 -0
  258. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +190 -0
  259. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +478 -0
  260. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +166 -0
  261. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +314 -0
  262. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +168 -0
  263. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +170 -0
  264. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +134 -0
  265. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +137 -0
  266. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +89 -0
  267. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +85 -0
  268. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
  269. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +132 -0
  270. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +74 -0
  271. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +758 -0
  272. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +120 -0
  273. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +31 -0
  274. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +46 -0
  275. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +102 -0
  276. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +46 -0
  277. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +70 -0
  278. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +72 -0
  279. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +120 -0
  280. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +183 -0
  281. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +41 -0
  282. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +28 -0
  283. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +30 -0
  284. data/lib/hecks/ports/persistence/plugins/era/translation.rb +11 -0
  285. data/lib/hecks/ports/persistence/plugins/era.rb +48 -0
  286. data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
  287. data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
  288. data/lib/hecks/ports/persistence.port +4 -0
  289. data/lib/hecks/ports/persistence.rb +39 -0
  290. data/lib/hecks/ports/projection.port +4 -0
  291. data/lib/hecks/ports/projection.rb +96 -0
  292. data/lib/hecks/ports/query/in_memory.rb +60 -0
  293. data/lib/hecks/ports/query/ordering.rb +41 -0
  294. data/lib/hecks/ports/query.rb +36 -0
  295. data/lib/hecks/ports.rb +27 -0
  296. data/lib/hecks/projections/diagrams.rb +642 -0
  297. data/lib/hecks/projections/ir.rb +18 -0
  298. data/lib/hecks/projections/model/deviations.rb +98 -0
  299. data/lib/hecks/projections/model.rb +145 -0
  300. data/lib/hecks/projections/oidc.rb +110 -0
  301. data/lib/hecks/projections/parser_table.rb +159 -0
  302. data/lib/hecks/projections/reference.rb +38 -0
  303. data/lib/hecks/projections/shape.rb +44 -0
  304. data/lib/hecks/projections/statements.rb +110 -0
  305. data/lib/hecks/projections/vocabulary.rb +100 -0
  306. data/lib/hecks/projections.rb +32 -0
  307. data/lib/hecks/projector/cli_projector.rb +446 -0
  308. data/lib/hecks/projector/docs_projector.rb +321 -0
  309. data/lib/hecks/projector/exporter.rb +158 -0
  310. data/lib/hecks/projector/ir_projector.rb +18 -0
  311. data/lib/hecks/projector/narrate_projector.rb +243 -0
  312. data/lib/hecks/projector/target.rb +97 -0
  313. data/lib/hecks/projector.rb +186 -0
  314. data/lib/hecks/query_ir.rb +411 -0
  315. data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
  316. data/lib/hecks/query_specification/common/comparators.rb +25 -0
  317. data/lib/hecks/query_specification/common/comparison.rb +174 -0
  318. data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
  319. data/lib/hecks/query_specification/common/dsl.rb +58 -0
  320. data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
  321. data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
  322. data/lib/hecks/query_specification/common/null_policy.rb +92 -0
  323. data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
  324. data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
  325. data/lib/hecks/query_specification/common/options.rb +32 -0
  326. data/lib/hecks/query_specification/common/order_by.rb +9 -0
  327. data/lib/hecks/query_specification/common/specification.rb +10 -0
  328. data/lib/hecks/query_specification/common/where_clause.rb +9 -0
  329. data/lib/hecks/query_specification/field_path.rb +107 -0
  330. data/lib/hecks/query_specification/hop_path.rb +132 -0
  331. data/lib/hecks/query_specification/read_model/specification.rb +18 -0
  332. data/lib/hecks/query_specification.rb +14 -0
  333. data/lib/hecks/rendering.rb +48 -0
  334. data/lib/hecks/router/namespace_installer.rb +157 -0
  335. data/lib/hecks/router.rb +70 -0
  336. data/lib/hecks/runtime/aggregate_lock.rb +45 -0
  337. data/lib/hecks/runtime/boot_gates.rb +41 -0
  338. data/lib/hecks/runtime/caller.rb +66 -0
  339. data/lib/hecks/runtime/capability_graph.rb +44 -0
  340. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +135 -0
  341. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +260 -0
  342. data/lib/hecks/runtime/command_interpreter.rb +497 -0
  343. data/lib/hecks/runtime/command_rules/admissibility.rb +357 -0
  344. data/lib/hecks/runtime/command_rules/arithmetic.rb +260 -0
  345. data/lib/hecks/runtime/command_rules/authorization.rb +65 -0
  346. data/lib/hecks/runtime/command_rules/emission.rb +34 -0
  347. data/lib/hecks/runtime/command_rules/references.rb +189 -0
  348. data/lib/hecks/runtime/command_rules.rb +28 -0
  349. data/lib/hecks/runtime/dependency_planning.rb +245 -0
  350. data/lib/hecks/runtime/dispatcher.rb +301 -0
  351. data/lib/hecks/runtime/entity_element.rb +253 -0
  352. data/lib/hecks/runtime/entity_interpreter.rb +299 -0
  353. data/lib/hecks/runtime/errors.rb +122 -0
  354. data/lib/hecks/runtime/event.rb +51 -0
  355. data/lib/hecks/runtime/identity.rb +156 -0
  356. data/lib/hecks/runtime/instance.rb +175 -0
  357. data/lib/hecks/runtime/interpreting.rb +90 -0
  358. data/lib/hecks/runtime/loader.rb +184 -0
  359. data/lib/hecks/runtime/policy_interpreter.rb +366 -0
  360. data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
  361. data/lib/hecks/runtime/query_interpreter.rb +286 -0
  362. data/lib/hecks/runtime/reaction_invocation.rb +253 -0
  363. data/lib/hecks/runtime/read_model_interpreter.rb +341 -0
  364. data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
  365. data/lib/hecks/runtime/reference_hop.rb +99 -0
  366. data/lib/hecks/runtime/refusal_wording.rb +126 -0
  367. data/lib/hecks/runtime/registry/saga_persistence.rb +142 -0
  368. data/lib/hecks/runtime/registry/verification.rb +246 -0
  369. data/lib/hecks/runtime/registry.rb +279 -0
  370. data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
  371. data/lib/hecks/runtime/routing.rb +96 -0
  372. data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
  373. data/lib/hecks/runtime/saga_interpreter.rb +503 -0
  374. data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -0
  375. data/lib/hecks/runtime/tenant_check.rb +84 -0
  376. data/lib/hecks/runtime/tenant_scope.rb +55 -0
  377. data/lib/hecks/runtime/value/admission.rb +130 -0
  378. data/lib/hecks/runtime/value/coercion.rb +565 -0
  379. data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
  380. data/lib/hecks/runtime/value.rb +125 -0
  381. data/lib/hecks/runtime.rb +107 -0
  382. data/lib/hecks/storehouse.rb +632 -0
  383. data/lib/hecks/version.rb +16 -0
  384. data/lib/hecks/vocabulary.rb +218 -0
  385. data/lib/hecks.rb +123 -6
  386. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
  387. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
  388. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
  389. metadata +412 -222
  390. data/bin/hecks +0 -7
  391. data/bin/hecks-package +0 -65
  392. data/bin/hecks_console +0 -12
  393. data/bin/hecks_serverless +0 -6
  394. data/lib/cli/build.rb +0 -14
  395. data/lib/cli/command_runner.rb +0 -28
  396. data/lib/cli/console.rb +0 -10
  397. data/lib/cli/generate.rb +0 -37
  398. data/lib/cli/hecks-cli.rb +0 -27
  399. data/lib/cli/test.rb +0 -57
  400. data/lib/console/commands.rb +0 -8
  401. data/lib/console/hecks-console.rb +0 -1
  402. data/lib/packager/README.md +0 -0
  403. data/lib/packager/app_runner.rb +0 -21
  404. data/lib/packager/args.rb +0 -26
  405. data/lib/packager/compatibility/fixnum.rb +0 -6
  406. data/lib/packager/hecks.rb +0 -39
  407. data/lib/packager/query_runner.rb +0 -21
  408. data/lib/packager/resources/Dockerfile +0 -11
  409. data/lib/packager/resources/app_binary +0 -7
  410. data/lib/packager/resources/bundle_config +0 -3
  411. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
  412. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
  413. data/lib/serverless/Domain +0 -32
  414. data/lib/serverless/cli.rb +0 -75
  415. data/lib/serverless/resources/command_name.js +0 -5
  416. data/lib/serverless/resources/environment.js +0 -7
  417. data/lib/serverless/resources/handler.js.tt +0 -28
  418. data/lib/serverless/resources/run_binary.js +0 -22
  419. data/lib/serverless/resources/serverless.yml +0 -20
@@ -0,0 +1,642 @@
1
+ require_relative "../projector"
2
+
3
+ module Hecks
4
+ module Projections
5
+ # A DOMAIN'S OWN SHAPE, PROJECTED AS MERMAID DIAGRAMS — the same
6
+ # trick `Projections::Reference`/`DocsProjector` already play for
7
+ # prose, one level further: a diagram generated FROM the
8
+ # declaration can't drift from it the way a hand-drawn one
9
+ # inevitably does, because there is no second copy to forget to
10
+ # update.
11
+ #
12
+ # MERMAID, NOT GRAPHVIZ (the two considered) — every diagram type
13
+ # below has a Mermaid form purpose-built for exactly what the
14
+ # underlying construct already is (a `lifecycle` IS a state
15
+ # machine, `has_many`/`belongs_to` already speaks in cardinality,
16
+ # `emits`/`trigger` already IS a directed graph), and the output is
17
+ # plain text that renders natively wherever this project's own docs
18
+ # already live — GitHub markdown, this repo's generated docs, Claude
19
+ # 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
21
+ # port) to become anything viewable, which is a real dependency this
22
+ # repository's own discipline (see rust/parser's Cargo.toml: "no
23
+ # dependency earns its way past std") would rather not take just to
24
+ # draw a diagram.
25
+ #
26
+ # FOUR DIAGRAM KINDS, one file each per domain except lifecycles
27
+ # (one per lifecycle-bearing construct, since that's how a reader
28
+ # actually reaches for it — looking at ONE aggregate's states, not
29
+ # every aggregate's at once):
30
+ #
31
+ # <Name>_lifecycle.mmd stateDiagram-v2 one per lifecycle
32
+ # relationships.mmd erDiagram the whole domain's has_many/
33
+ # has_one/belongs_to/reference_to
34
+ # dispatch.mmd flowchart the whole domain's command
35
+ # emits -> policy trigger chains
36
+ # roles.mmd flowchart every role that issues a
37
+ # command, wired to every
38
+ # command it issues
39
+ # ports.mmd flowchart every port operation, which
40
+ # aggregate exposes it, which
41
+ # aggregate it routes to: (if
42
+ # any), and what it emits
43
+ # read_models.mmd flowchart every read_model, and every
44
+ # aggregate it's assembled
45
+ # from — the read-side
46
+ # complement to relationships.mmd
47
+ # <Name>_surface.mmd flowchart one per aggregate/entity that
48
+ # declares at least one command
49
+ # or query — everything you can
50
+ # DO to it and ASK about it,
51
+ # AND what each command WRITES,
52
+ # in one place
53
+ # <Name>_saga.mmd stateDiagram-v2 one per process_manager —
54
+ # its own states, and what
55
+ # each transition dispatches
56
+ # elsewhere in the domain
57
+ # frameworks.mmd flowchart every OTHER domain this one
58
+ # depends on — a shared
59
+ # framework it `uses_framework`,
60
+ # or a domain a policy reaches
61
+ # `across` — the one diagram
62
+ # here that looks OUTWARD past
63
+ # this domain's own boundary
64
+ #
65
+ # CONSTRUCT NAMES (aggregate/entity/command/event) ARE USED BARE,
66
+ # UNSANITIZED, as Mermaid node/entity ids — safe because this
67
+ # language's own word grammar only ever admits simple CamelCase/
68
+ # snake_case identifiers there (confirmed: no space or punctuation
69
+ # appears in any real aggregate/command/event name across the corpus
70
+ # this projects from). A `role:` STRING IS FREE TEXT, though — the
71
+ # real corpus already has "Back office"/"Vault officer"/"Branch
72
+ # clerk" — so `roles.mmd` is the one diagram here that sanitizes a
73
+ # name into an id (`role_id`) while keeping the real string as the
74
+ # node's own displayed label.
75
+ module Diagrams
76
+ extend Hecks::Projector::Target
77
+
78
+ projects_as :diagrams, emits: :files
79
+
80
+ module_function
81
+
82
+ def call(bluebook:, options: {})
83
+ files = {}
84
+
85
+ holders_with_lifecycle(bluebook).each do |holder|
86
+ files["#{holder.hecks_name}_lifecycle.mmd"] = lifecycle_diagram(bluebook, holder)
87
+ end
88
+
89
+ if (diagram = relationship_diagram(bluebook))
90
+ files["relationships.mmd"] = diagram
91
+ end
92
+
93
+ if (diagram = dispatch_diagram(bluebook))
94
+ files["dispatch.mmd"] = diagram
95
+ end
96
+
97
+ if (diagram = roles_diagram(bluebook))
98
+ files["roles.mmd"] = diagram
99
+ end
100
+
101
+ if (diagram = ports_diagram(bluebook))
102
+ files["ports.mmd"] = diagram
103
+ end
104
+
105
+ if (diagram = read_model_diagram(bluebook))
106
+ files["read_models.mmd"] = diagram
107
+ end
108
+
109
+ holders(bluebook).each do |holder|
110
+ next if holder.commands.empty? && holder.queries.empty?
111
+
112
+ files["#{holder.hecks_name}_surface.mmd"] = surface_diagram(bluebook, holder)
113
+ end
114
+
115
+ bluebook.process_managers.each do |saga|
116
+ files["#{saga.hecks_name}_saga.mmd"] = saga_diagram(bluebook, saga)
117
+ end
118
+
119
+ if (diagram = frameworks_diagram(bluebook, options[:hecksagon]))
120
+ files["frameworks.mmd"] = diagram
121
+ end
122
+
123
+ files
124
+ end
125
+
126
+ # ── shared ────────────────────────────────────────────────────────
127
+
128
+ # AN ENTITY CAN CARRY ITS OWN LIFECYCLE, RELATIONSHIP, OR COMMAND
129
+ # TOO — its own `lifecycle`/`reference_to`/`command` block,
130
+ # addressed through its holding aggregate the same way
131
+ # `DocsProjector` already treats an aggregate and its entities
132
+ # alike. Walking both here means a domain's entity gaining any of
133
+ # these needs no change to this file.
134
+ def holders(bluebook)
135
+ bluebook.aggregates.flat_map { |aggregate| [aggregate, *aggregate.entities] }
136
+ end
137
+
138
+ def holders_with_lifecycle(bluebook) = holders(bluebook).select(&:lifecycle)
139
+
140
+ # `chapter_name` DRIVES THE RE-RUN HINT ALWAYS — that's the one
141
+ # argument `bin/project_diagrams` actually takes, regardless of
142
+ # which single aggregate/entity `subject` happens to name. Passing
143
+ # the wrong one here once already produced a real, committed
144
+ # `Order_lifecycle.mmd` telling a reader to run
145
+ # `bin/project_diagrams <domain-path> Order` — a chapter name
146
+ # Hecks.boot has never heard of.
147
+ def header(chapter_name, subject)
148
+ <<~HEADER
149
+ %% GENERATED by bin/project_diagrams from #{subject} — DO NOT EDIT BY HAND.
150
+ %% Re-run `bin/project_diagrams <domain-path> #{chapter_name}` after any change.
151
+ HEADER
152
+ end
153
+
154
+ # ── lifecycle -> stateDiagram-v2 ─────────────────────────────────
155
+
156
+ def lifecycle_diagram(bluebook, holder)
157
+ lifecycle = holder.lifecycle
158
+ edges = lifecycle.transitions.flat_map do |command_name, transition|
159
+ Array(transition.from).map { |from_state| " #{from_state} --> #{transition.target}: #{command_name}" }
160
+ end
161
+
162
+ subject = "#{holder.hecks_name}'s own declared lifecycle (field: #{lifecycle.field})"
163
+ <<~MERMAID
164
+ #{header(bluebook.name, subject)}stateDiagram-v2
165
+ [*] --> #{lifecycle.default}
166
+ #{edges.join("\n")}
167
+ MERMAID
168
+ end
169
+
170
+ # ── relationships -> erDiagram ───────────────────────────────────
171
+
172
+ # STANDARD CROW'S-FOOT READING, the same convention every ORM's own
173
+ # ERD generator (Rails' erd gem included) already uses:
174
+ # `has_many`/`has_one` are read from the OWNING side — one Holder
175
+ # 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
178
+ # how many holders point back at it; "many" is the honest default
179
+ # absent a declared uniqueness rule this language doesn't expose.
180
+ # `optional?` only ever softens the side that can genuinely be
181
+ # absent (a nilable reference, an empty has_one) — never the
182
+ # crow's-foot "many" marker, which is a structural fact independent
183
+ # of any one instance's optionality.
184
+ def relationship_diagram(bluebook)
185
+ edges = holders(bluebook).flat_map do |holder|
186
+ holder.attributes.select(&:reference?).map { |attribute| relationship_edge(holder, attribute) }
187
+ end
188
+ return nil if edges.empty?
189
+
190
+ subject = "#{bluebook.name}'s own declared reference_to/belongs_to/has_many/has_one attributes"
191
+ "#{header(bluebook.name, subject)}erDiagram\n#{edges.join("\n")}\n"
192
+ end
193
+
194
+ def relationship_edge(holder, attribute)
195
+ target = attribute.type.target_name
196
+ case attribute.relationship
197
+ when "has_many"
198
+ %( #{holder.hecks_name} ||--o{ #{target} : "#{attribute.name}")
199
+ when "has_one"
200
+ %( #{holder.hecks_name} ||--#{attribute.optional? ? 'o|' : '||'} #{target} : "#{attribute.name}")
201
+ when "belongs_to", "reference_to"
202
+ %( #{target} #{attribute.optional? ? '|o' : '||'}--o{ #{holder.hecks_name} : "#{attribute.name}")
203
+ end
204
+ end
205
+
206
+ # ── dispatch -> flowchart ─────────────────────────────────────────
207
+
208
+ # A COMMAND NODE, STADIUM-SHAPED (`(["..."])`); AN EVENT NODE,
209
+ # HEXAGONAL (`{{"..."}}`) — one visual vocabulary for "a thing
210
+ # someone does" versus "a fact that happened", matching the
211
+ # language's own verb/event distinction. Command ids are qualified
212
+ # by their owning aggregate (`cmd_Order_Purchase`) since two
213
+ # aggregates may share a command name; event ids are bare
214
+ # (`evt_PizzaCreated`) since an event is this domain's own
215
+ # addressing key, the same way `policy.on_event` reaches it.
216
+ def dispatch_diagram(bluebook)
217
+ lines = []
218
+
219
+ holders(bluebook).each do |holder|
220
+ holder.commands.each do |command|
221
+ command.emits.each { |event| lines << emits_edge(holder, command, event) }
222
+ end
223
+ end
224
+
225
+ bluebook.policies.each { |policy| lines << trigger_edge(policy) }
226
+
227
+ lines.compact!
228
+ return nil if lines.empty?
229
+
230
+ subject = "#{bluebook.name}'s own declared commands' emits and policies' on/trigger"
231
+ "#{header(bluebook.name, subject)}flowchart LR\n#{lines.uniq.join("\n")}\n"
232
+ end
233
+
234
+ def emits_edge(holder, command, event)
235
+ %( #{command_node(holder.hecks_name, command.hecks_name)} -->|emits| #{event_node(event)})
236
+ end
237
+
238
+ # `on_event` IS SOMETIMES AGGREGATE-QUALIFIED
239
+ # (`"Account.AccountFrozen"`) AND SOMETIMES BARE
240
+ # (`"CustomerSuspended"`) in the real corpus — `emits` never is,
241
+ # so this always matches against the bare tail, the same
242
+ # normalization a reader has to do by eye today.
243
+ #
244
+ # A TRIGGER CROSSING INTO ANOTHER DOMAIN (`policy.target_domain`)
245
+ # still draws — the target command just has no incoming `emits`
246
+ # edge of its own here, which honestly shows "dispatch continues
247
+ # elsewhere" rather than silently dropping the edge. The label
248
+ # names which domain, so that's not a dead end on the page either.
249
+ def trigger_edge(policy)
250
+ bare_event = policy.on_event.to_s.split(".").last
251
+ aggregate_name, command_name = policy.trigger_command.to_s.split(".", 2)
252
+ label = policy.target_domain ? "triggers in #{policy.target_domain}" : "triggers"
253
+ %( #{event_node(bare_event)} -->|#{label}| #{command_node(aggregate_name, command_name)})
254
+ end
255
+
256
+ def command_node(aggregate_name, command_name)
257
+ %(cmd_#{aggregate_name}_#{command_name}(["#{aggregate_name}.#{command_name}"]))
258
+ end
259
+
260
+ def event_node(event_name) = %(evt_#{event_name}{{"#{event_name}"}})
261
+
262
+ # ── roles -> flowchart ────────────────────────────────────────────
263
+
264
+ # WHO ISSUES WHAT, ACROSS THE WHOLE DOMAIN — data no existing
265
+ # projection draws at all today (the reference pages' own
266
+ # `command_entry` only ever prints a command's role as a single
267
+ # line of prose, never assembled across commands). A command with
268
+ # no declared `role` draws nothing — there is no fact to state.
269
+ # Circle-shaped so a role reads as "who" beside `dispatch.mmd`'s
270
+ # stadium ("what someone does") and hexagon ("what happened").
271
+ def roles_diagram(bluebook)
272
+ lines = holders(bluebook).flat_map do |holder|
273
+ holder.commands.select(&:role).map { |command| role_edge(holder, command) }
274
+ end
275
+ return nil if lines.empty?
276
+
277
+ subject = "#{bluebook.name}'s own declared command roles"
278
+ "#{header(bluebook.name, subject)}flowchart LR\n#{lines.uniq.join("\n")}\n"
279
+ end
280
+
281
+ def role_edge(holder, command)
282
+ %( #{role_node(command.role)} -->|issues| #{command_node(holder.hecks_name, command.hecks_name)})
283
+ end
284
+
285
+ def role_node(role_name) = %(#{role_id(role_name)}((#{role_name})))
286
+
287
+ # A ROLE NAME IS FREE TEXT ("Back office", "Vault officer") —
288
+ # unlike every other name this file uses as a bare id, this one
289
+ # has to be sanitized to become a legal Mermaid identifier. The
290
+ # real string still appears as the node's own label
291
+ # (`role_node`); only the id is mangled.
292
+ def role_id(role_name) = "role_#{role_name.to_s.gsub(/[^A-Za-z0-9]+/, '_')}"
293
+
294
+ # ── ports -> flowchart ───────────────────────────────────────────
295
+
296
+ # A PORT OPERATION IS A BOUNDARY TRANSLATION, NOT A VERB OR A FACT —
297
+ # its own reference page says so plainly ("the builder behind it
298
+ # defines no `given` or `sets`, so an operation cannot read
299
+ # aggregate state or mutate a record itself"), so it gets a third
300
+ # shape, a trapezoid, beside `dispatch.mmd`'s stadium/hexagon
301
+ # vocabulary. An aggregate drawn as a `to:` target is a cylinder —
302
+ # state landing somewhere, the same reason a data store gets one
303
+ # in an ordinary flowchart.
304
+ #
305
+ # TWO EDGE KINDS PER OPERATION: a dotted "exposes" edge from the
306
+ # aggregate the port hangs off (always present — a port always
307
+ # belongs to exactly one aggregate), and a solid "to:" edge to
308
+ # 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).
311
+ # `emits` reuses `dispatch.mmd`'s own `event_node` unchanged — the
312
+ # same fact, reached from a different direction.
313
+ #
314
+ # `bluebook.aggregates`, NOT the shared `holders` — unlike a
315
+ # lifecycle/relationship/command, a port belongs to an AGGREGATE
316
+ # only; an entity has no `ports` method at all (confirmed: calling
317
+ # it raises, it isn't just always empty), so walking entities here
318
+ # the way every other diagram in this file does would crash on
319
+ # the first entity-bearing domain.
320
+ def ports_diagram(bluebook)
321
+ lines = bluebook.aggregates.flat_map do |holder|
322
+ holder.ports.flat_map { |port| port.operations.map { |operation| port_edges(holder, port, operation) } }
323
+ end.flatten
324
+
325
+ return nil if lines.empty?
326
+
327
+ subject = "#{bluebook.name}'s own declared port operations (which aggregate exposes each, its to:, and its emits)"
328
+ "#{header(bluebook.name, subject)}flowchart LR\n#{lines.uniq.join("\n")}\n"
329
+ end
330
+
331
+ def port_edges(holder, port, operation)
332
+ op = port_operation_node(holder.hecks_name, port.name, operation.hecks_name)
333
+ edges = [" #{holder.hecks_name}[(#{holder.hecks_name})] -.->|exposes| #{op}"]
334
+ edges << " #{op} -->|to: #{operation.to}| #{operation.to}[(#{operation.to})]" if operation.to
335
+ operation.emits.each { |event| edges << " #{op} -->|emits| #{event_node(event)}" }
336
+ edges
337
+ end
338
+
339
+ def port_operation_node(aggregate_name, port_name, operation_name)
340
+ id = "op_#{aggregate_name}_#{port_name}_#{operation_name}"
341
+ %(#{id}[/"#{port_name}.#{operation_name}"/])
342
+ end
343
+
344
+ # ── read models -> flowchart ─────────────────────────────────────
345
+
346
+ # THE READ-SIDE COMPLEMENT TO `relationships.mmd` — that diagram
347
+ # shows how aggregates reference each other for WRITES
348
+ # (`has_many`/`belongs_to`/`reference_to`); this shows how a
349
+ # `read_model` ASSEMBLES data for READS, from
350
+ # `aggregate_heads` — the same list `where`/`group_by`/`order_by`
351
+ # all operate over, and the one fact every read_model has
352
+ # regardless of whether it's rooted (`reference_target`) or
353
+ # gathers heads with no root at all (a rootless read model, real
354
+ # in the corpus: `AccountsByKind`).
355
+ #
356
+ # A READ MODEL IS A SUBROUTINE SHAPE (`[[...]]`, "a predefined
357
+ # process") — a fourth shape, beside `ports.mmd`'s trapezoid and
358
+ # `dispatch.mmd`'s stadium/hexagon: not a verb, not a fact, not a
359
+ # boundary translation, but a standing, reusable view. Every
360
+ # aggregate it draws from is a cylinder — the same "state lands
361
+ # somewhere" shape `ports.mmd`'s `to:` target already uses, and
362
+ # the same bare id, so an aggregate feeding several read_models
363
+ # (real in banking: `Account` feeds four) merges into one node
364
+ # across the whole diagram.
365
+ #
366
+ # THE LABEL NAMES THE SHAPE OF THE ANSWER, NOT JUST THE NAME —
367
+ # `(count)`/`(median: field)` for the two real aggregations in the
368
+ # corpus, nothing appended for an ordinary row-returning
369
+ # read_model. Still MVP scope: `where`/`group_by`/`order_by`
370
+ # aren't drawn at all yet — real facts, not invented, just not
371
+ # this diagram's job yet.
372
+ def read_model_diagram(bluebook)
373
+ lines = bluebook.read_models.flat_map { |read_model| read_model_edges(read_model) }
374
+ return nil if lines.empty?
375
+
376
+ subject = "#{bluebook.name}'s own declared read_models and the aggregates each is assembled from"
377
+ "#{header(bluebook.name, subject)}flowchart LR\n#{lines.uniq.join("\n")}\n"
378
+ end
379
+
380
+ def read_model_edges(read_model)
381
+ shape = read_model.to_h
382
+ node = %(rm_#{shape[:name]}[["#{read_model_label(shape)}"]])
383
+ Array(shape[:aggregate_heads]).map do |head|
384
+ # QUOTED, NOT BARE — an edge label containing `[` or `]`
385
+ # (`accounts[]`, marking the "many" side) breaks Mermaid's own
386
+ # `|label|` parser outright if left unquoted: it reads the
387
+ # `[` as the START OF A NEW NODE SHAPE mid-label, not text.
388
+ # Confirmed live against the real parser before this quoting
389
+ # existed — every OTHER edge label in this file happens to be
390
+ # a bare word or already-quoted string, so this is the one
391
+ # spot that needed it.
392
+ label = head[:many] ? "#{head[:as]}[]" : head[:as]
393
+ %( #{head[:aggregate]}[(#{head[:aggregate]})] -->|"#{label}"| #{node})
394
+ end
395
+ end
396
+
397
+ def read_model_label(shape)
398
+ return "#{shape[:name]} (count)" if shape[:count]
399
+ return "#{shape[:name]} (median: #{shape[:median_field]})" if shape[:median_field]
400
+
401
+ shape[:name]
402
+ end
403
+
404
+ # ── surface -> flowchart ─────────────────────────────────────────
405
+
406
+ # "WHAT CAN I DO TO THIS, WHAT CAN I ASK ABOUT IT" — one file per
407
+ # holder, unlike every other diagram here: `dispatch.mmd` already
408
+ # shows a command's own onward reaction chain, but never an
409
+ # aggregate's own FULL command/query menu in one place, and
410
+ # `roles.mmd` shows who issues a command without saying what else
411
+ # that same aggregate answers. This is the one diagram meant to
412
+ # be read starting from the aggregate, not from a verb or a fact.
413
+ #
414
+ # A QUERY IS A DIAMOND — a fifth shape, beside `dispatch.mmd`'s
415
+ # stadium/hexagon, `ports.mmd`'s trapezoid, and `read_models.mmd`'s
416
+ # subroutine: a question with an answer, not a verb that changes
417
+ # anything. Command edges are solid ("does"); query edges are
418
+ # dotted ("asks") — the same solid/dotted split `ports.mmd`
419
+ # already uses for "routes to:" versus "exposes".
420
+ #
421
+ # A WRITE TARGET IS A PLAIN RECTANGLE — a sixth shape, the first
422
+ # here with no special bracket at all: an attribute is the
423
+ # smallest, most passive thing this vocabulary names, a single
424
+ # field living INSIDE the cylinder rather than a bounded thing of
425
+ # its own. `command.mutations` (`sets`/`increment`/`decrement`/
426
+ # `append`) was invisible everywhere before this — not just in a
427
+ # diagram, in ANY projection, including the prose ones — despite
428
+ # being the single densest fact in the whole IR (53 real
429
+ # mutations across pizzas + banking). `dispatch.mmd` draws what a
430
+ # command EMITS; this draws what it WRITES, the other half of
431
+ # "what actually happens" a command never showed before.
432
+ #
433
+ # THE SAME ATTRIBUTE NODE MERGES ACROSS COMMANDS — real in
434
+ # banking: `Account.Credit` and `Account.Debit` both point at the
435
+ # same `balance` node, the same "one node, several incoming
436
+ # edges" merge `read_models.mmd` already does for an aggregate
437
+ # fed by several read_models.
438
+ #
439
+ # THE LABEL NAMES THE REAL SOURCE, NOT JUST THE VERB — an
440
+ # increment/decrement/set almost always takes its value from an
441
+ # argument, but not always the SAME-NAMED one: real in banking,
442
+ # `Account.Credit`'s own `balance` is incremented by its
443
+ # `amount` argument, and `LedgerEntry.Amend`'s own `amount` is
444
+ # incremented by its `adjustment` argument. A literal source
445
+ # (pizzas' own `Order.Purchase` sets `status` to the literal
446
+ # `"sold"`, not an argument at all) is named as verbatim as
447
+ # 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
449
+ # stating (real: `Order.AddTopping` appends `name, amount`).
450
+ def surface_diagram(bluebook, holder)
451
+ lines = holder.commands.map { |command| " #{holder.hecks_name}[(#{holder.hecks_name})] -->|does| #{command_node(holder.hecks_name, command.hecks_name)}" }
452
+ lines += holder.commands.flat_map { |command| command.mutations.map { |mutation| mutation_edge(holder, command, mutation) } }
453
+ lines += holder.queries.map { |query| " #{holder.hecks_name}[(#{holder.hecks_name})] -.->|asks| #{query_node(holder.hecks_name, query.hecks_name)}" }
454
+
455
+ subject = "#{holder.hecks_name}'s own declared commands (and what each writes) and queries"
456
+ "#{header(bluebook.name, subject)}flowchart LR\n#{lines.uniq.join("\n")}\n"
457
+ end
458
+
459
+ def query_node(aggregate_name, query_name)
460
+ %(qry_#{aggregate_name}_#{query_name}{"#{aggregate_name}.#{query_name}"})
461
+ end
462
+
463
+ def mutation_edge(holder, command, mutation)
464
+ shape = mutation.to_h
465
+ label = mutation_label(shape)
466
+ target = attribute_node(holder.hecks_name, shape[:target])
467
+ %( #{command_node(holder.hecks_name, command.hecks_name)} -->|"#{label}"| #{target})
468
+ end
469
+
470
+ def mutation_label(shape)
471
+ verb = "#{shape[:op]}s"
472
+ # `fields:` (not `source:`) IS the multi-binding shape
473
+ # (`Mutation#to_h`'s own `[:append, :delegate, :corrects]`
474
+ # branch) — checked by the KEY'S PRESENCE, not by re-listing
475
+ # which ops use it a second time here, the same lesson
476
+ # `Change.op`'s own `admits: Vocabulary::MutationOp` already
477
+ # drew (command.bluebook's own comment): a second list of "the
478
+ # ops that mean multi-binding" is exactly the kind of copy that
479
+ # drifts — `:delegate` already carried this shape with nothing
480
+ # here reading it correctly, caught only once `:corrects` gave
481
+ # banking's own real diagrams a fields:-shaped mutation to
482
+ # actually render.
483
+ detail = shape[:fields] ? shape[:fields].keys.join(", ") : mutation_source_detail(shape[:source])
484
+ "#{verb}: #{detail}"
485
+ end
486
+
487
+ # A LITERAL VALUE CAN CONTAIN A DOUBLE QUOTE OF ITS OWN — real in
488
+ # banking: `Customer.Reinstate` sets `standing` to a rendered
489
+ # value-object literal, `{:value=>"good"}`, whose own embedded `"`
490
+ # broke this label's outer `|"..."|` quoting outright (caught by
491
+ # running the real generated output through mermaid.parse(), not
492
+ # by eye — the same way `read_models.mmd`'s own unquoted `[]` bug
493
+ # was caught). Swapped for a single quote here rather than
494
+ # escaped, the same "state it, don't invent it, just make it
495
+ # legal Mermaid" trade `read_models.mmd`'s own quoting fix made.
496
+ def mutation_source_detail(source)
497
+ case source[:kind]
498
+ when "literal" then "'#{source[:value].to_s.tr('"', "'")}'"
499
+ when "argument" then source[:name]
500
+ else source[:kind] # a source kind this file has no real corpus example of yet — named, not hidden
501
+ end
502
+ end
503
+
504
+ def attribute_node(holder_name, attribute_name)
505
+ %(attr_#{holder_name}_#{attribute_name}[#{attribute_name}])
506
+ end
507
+
508
+ # ── sagas -> stateDiagram-v2 ─────────────────────────────────────
509
+
510
+ # A SAGA HAS A LIFECYCLE TOO — the same `stateDiagram-v2` shape
511
+ # `lifecycle_diagram` already draws, one file per process_manager
512
+ # the same way lifecycle is one file per lifecycle-bearing holder.
513
+ # What's different is the label: a lifecycle's own edge is labeled
514
+ # by the COMMAND that causes it (an aggregate transitions because
515
+ # something was DONE to it); a saga's edge is labeled by the EVENT
516
+ # that causes it (a saga advances because something HAPPENED,
517
+ # possibly nowhere near the saga itself) — the same command/event
518
+ # split `dispatch.mmd`'s own stadium/hexagon vocabulary already
519
+ # draws, here spent on which noun labels a stateDiagram-v2 edge
520
+ # instead.
521
+ #
522
+ # THE LABEL ALSO NAMES WHAT THE TRANSITION DISPATCHES — a fact no
523
+ # existing diagram states for a saga at all: a lifecycle's own
524
+ # edge only ever names the one command that caused it; a saga's
525
+ # edge can fire several commands at once (real in banking:
526
+ # Settlement's own AccountDebited handler dispatches both
527
+ # Transfer.Debited and Account.Credit). Confirmed real in the
528
+ # corpus: no saga dispatch ever declares a `to:`/`target_domain`
529
+ # of its own (unlike a policy's `across`) — every command a saga
530
+ # fires lands inside its own bluebook chapter, so this never needs
531
+ # `dispatch.mmd`'s own "triggers in X" cross-domain label.
532
+ #
533
+ # THE COMPENSATING LEG READS LIKE ANY OTHER — its own trigger is
534
+ # the literal string "refused" (`ProcessManager::REFUSED`, this
535
+ # language's own Trigger vocabulary), not invented text: a
536
+ # dispatch declined is exactly as real a cause of a state
537
+ # transition as an event announced, and the diagram states it
538
+ # exactly as verbatim as every other edge here does.
539
+ def saga_diagram(bluebook, saga)
540
+ edges = saga.handlers.map { |handler| saga_edge(handler, saga) }
541
+
542
+ subject = "#{saga.hecks_name}'s own declared states and what each transition dispatches " \
543
+ "(starts on #{saga.starts_on}, ends on #{saga.ends_on})"
544
+ <<~MERMAID
545
+ #{header(bluebook.name, subject)}stateDiagram-v2
546
+ [*] --> #{saga.states.first}
547
+ #{edges.join("\n")}
548
+ MERMAID
549
+ end
550
+
551
+ # THE REFUSED EDGE'S OWN DISPATCH LIST IS PARTLY DERIVED NOW —
552
+ # per-dispatch saga compensation (`compensates`) moved a saga's own
553
+ # compensating dispatches OFF the hand-written `on :refused` leg
554
+ # and onto whichever forward dispatch each one undoes, so
555
+ # `handler.dispatches` alone would render an EMPTY compensating
556
+ # edge for any saga using it — accurate to the DECLARATION, wrong
557
+ # about what the runtime actually does at refusal (it derives and
558
+ # fires every declared `compensates`, newest first). `saga` is
559
+ # passed through for exactly this — only the REFUSED handler needs
560
+ # it, every other edge's own `handler.dispatches` already says
561
+ # everything real about it.
562
+ def saga_edge(handler, saga)
563
+ label = handler.event_type
564
+ # DERIVED FIRST, then the hand-written body — the same order
565
+ # `SagaInterpreter#unwind` actually runs them in (every
566
+ # completed leg's own `compensates` before this leg's own
567
+ # hand-written dispatches), not declaration order on the page.
568
+ dispatched = handler.event_type == Bluebook::ProcessManager::REFUSED ? derived_compensations(saga) : []
569
+ dispatched += handler.dispatches.map(&:command_name)
570
+ label += " / dispatches #{dispatched.join(', ')}" unless dispatched.empty?
571
+
572
+ " #{handler.from_state} --> #{handler.to_state}: #{label}"
573
+ end
574
+
575
+ # Every `compensates` any forward dispatch in this saga declares,
576
+ # declaration order — the same commands `SagaInterpreter#unwind`
577
+ # derives and fires (newest-first, at actual refusal time; this
578
+ # diagram states them in declaration order, since it draws the
579
+ # saga's own shape, not one instance's own runtime history).
580
+ def derived_compensations(saga)
581
+ saga.handlers.flat_map { |handler| handler.dispatches.filter_map { |dispatch| dispatch.compensates&.command_name } }
582
+ end
583
+
584
+ # ── frameworks -> flowchart ─────────────────────────────────────
585
+
586
+ # EVERY OTHER DIAGRAM IN THIS FILE STAYS INSIDE ONE DOMAIN'S OWN
587
+ # BOUNDARY — this is the one that steps outside it. A real domain
588
+ # depends on another domain's own aggregates in exactly two ways:
589
+ # `uses_framework "X"` in its `.hecksagon` (`Hecksagon#framework_
590
+ # members`), which loads X's whole bluebook into THIS registry,
591
+ # unconditionally, the moment this domain boots; or a policy's own
592
+ # `across "X"` (`Policy#target_domain`), which only reaches X when
593
+ # the policy's declared event actually fires. Same underlying
594
+ # fact `dispatch.mmd`'s own `trigger_edge` already draws from the
595
+ # command's side ("triggers in X") — this draws it again from the
596
+ # DOMAIN's side, next to the structural `uses_framework` fact
597
+ # `dispatch.mmd` never sees at all (that lives in the `.hecksagon`,
598
+ # which no other diagram here is handed).
599
+ #
600
+ # NEITHER THIS DOMAIN NOR EACH DEPENDENCY GETS THE holders() TREATMENT
601
+ # — a whole domain is drawn as ONE cylinder, the same "a bounded,
602
+ # addressable thing" shape every other diagram here already spends
603
+ # on a single aggregate, just scaled up one level: a domain is a
604
+ # bigger box the same kind of box lives inside.
605
+ #
606
+ # DOTTED FOR `attaches`, SOLID FOR `reaches across` — the reverse
607
+ # of which fact is "always true" between the two: attaching a
608
+ # framework is a standing declaration, true every time this domain
609
+ # boots, so it gets the same dotted "this always belongs" treatment
610
+ # `ports.mmd` gives an aggregate's own `-.->|exposes|` edge.
611
+ # Reaching across only happens when a real policy actually fires —
612
+ # the same solid edge `dispatch.mmd`'s own `trigger_edge` already
613
+ # draws for the identical fact, kept solid here so the same
614
+ # relationship reads the same way in both diagrams.
615
+ #
616
+ # `options[:hecksagon]` IS THE ONE DIAGRAM IN THIS FILE THAT NEEDS
617
+ # MORE THAN `bluebook` — `framework_members` lives on the
618
+ # `Hecksagon`, a sibling IR object `bin/project_diagrams` already
619
+ # has in hand (`registry.hecksagon(chapter_name)`) but `bluebook`
620
+ # itself carries no reference to. No hecksagon handed in (an older
621
+ # caller, or a spec that doesn't care) just means no frameworks.mmd
622
+ # — same "nothing to state" skip every other diagram here already
623
+ # takes when its own underlying data is empty.
624
+ def frameworks_diagram(bluebook, hecksagon)
625
+ return nil unless hecksagon
626
+
627
+ lines = hecksagon.framework_members.map { |name| domain_edge(bluebook.name, "attaches", name, dotted: true) }
628
+ lines += bluebook.policies.filter_map(&:target_domain).uniq
629
+ .map { |name| domain_edge(bluebook.name, "reaches across", name, dotted: false) }
630
+ return nil if lines.empty?
631
+
632
+ subject = "#{bluebook.name}'s own declared uses_framework and cross-domain policy targets"
633
+ "#{header(bluebook.name, subject)}flowchart LR\n#{lines.uniq.join("\n")}\n"
634
+ end
635
+
636
+ def domain_edge(from, label, to, dotted:)
637
+ arrow = dotted ? "-.->" : "-->"
638
+ %( #{from}[(#{from})] #{arrow}|#{label}| #{to}[(#{to})])
639
+ end
640
+ end
641
+ end
642
+ end
@@ -0,0 +1,18 @@
1
+ require_relative "../projector"
2
+
3
+ module Hecks
4
+ module Projections
5
+ # The canonical IR, as a constant. The implementation already existed
6
+ # and is already golden-tested (`Projector::IRProjector`, registered
7
+ # as `:ir`) — this only gives it the constant spelling every other
8
+ # target has, by re-registering the SAME module under the same key.
9
+ #
10
+ # Deliberately not a new implementation: two things named `IR` that
11
+ # each rendered IR their own way is exactly the drift this namespace
12
+ # exists to avoid.
13
+ IR = Projector::IRProjector
14
+
15
+ IR.extend(Projector::Target)
16
+ IR.projects_as :ir, requires: Hecks::IR
17
+ end
18
+ end