hecks 0.2.0 → 0.3.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 (408) 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 +403 -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 +62 -0
  12. data/lib/hecks/adapters/driven/heki/journal.rb +51 -0
  13. data/lib/hecks/adapters/driven/heki/saga_store.rb +0 -0
  14. data/lib/hecks/adapters/driven/heki/snapshot.rb +38 -0
  15. data/lib/hecks/adapters/driven/heki.adapter +4 -0
  16. data/lib/hecks/adapters/driven/heki.rb +148 -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 +131 -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 +80 -0
  28. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +168 -0
  29. data/lib/hecks/adapters/driven/postgres.adapter +5 -0
  30. data/lib/hecks/adapters/driven/postgres.rb +391 -0
  31. data/lib/hecks/adapters/driven/postgres_era/lineage/era_store.rb +171 -0
  32. data/lib/hecks/adapters/driven/postgres_era/lineage/field_cache.rb +190 -0
  33. data/lib/hecks/adapters/driven/postgres_era/lineage/head_compiler.rb +429 -0
  34. data/lib/hecks/adapters/driven/postgres_era/lineage/mint_transaction.rb +166 -0
  35. data/lib/hecks/adapters/driven/postgres_era/lineage/provisioning.rb +286 -0
  36. data/lib/hecks/adapters/driven/postgres_era/lineage/resumable_backfill.rb +168 -0
  37. data/lib/hecks/adapters/driven/postgres_era/lineage/tail_merge.rb +163 -0
  38. data/lib/hecks/adapters/driven/postgres_era/lineage/transform_installer.rb +114 -0
  39. data/lib/hecks/adapters/driven/postgres_era/lineage.rb +137 -0
  40. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/coverage_check.rb +89 -0
  41. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/era_resolver.rb +82 -0
  42. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
  43. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/minter.rb +132 -0
  44. data/lib/hecks/adapters/driven/postgres_era/lineage_manager.rb +74 -0
  45. data/lib/hecks/adapters/driven/postgres_era.adapter +6 -0
  46. data/lib/hecks/adapters/driven/postgres_era.rb +671 -0
  47. data/lib/hecks/adapters/driven/prism.adapter +3 -0
  48. data/lib/hecks/adapters/driven/prism.rb +80 -0
  49. data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
  50. data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
  51. data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
  52. data/lib/hecks/adapters/driven/sqlite/codec.rb +63 -0
  53. data/lib/hecks/adapters/driven/sqlite/projection.rb +105 -0
  54. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +180 -0
  55. data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
  56. data/lib/hecks/adapters/driven/sqlite.rb +285 -0
  57. data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
  58. data/lib/hecks/adapters/driven/system_clock.rb +14 -0
  59. data/lib/hecks/adapters/driven.rb +36 -0
  60. data/lib/hecks/adapters.rb +6 -0
  61. data/lib/hecks/behaviors/dsl.rb +96 -0
  62. data/lib/hecks/behaviors/expectations.rb +280 -0
  63. data/lib/hecks/behaviors/ir.rb +31 -0
  64. data/lib/hecks/behaviors/rspec.rb +42 -0
  65. data/lib/hecks/behaviors/runner.rb +96 -0
  66. data/lib/hecks/behaviors.rb +25 -0
  67. data/lib/hecks/bluebook/aggregate.rb +108 -0
  68. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
  69. data/lib/hecks/bluebook/assembly/build.rb +48 -0
  70. data/lib/hecks/bluebook/assembly/contract.rb +112 -0
  71. data/lib/hecks/bluebook/assembly/contracts.rb +409 -0
  72. data/lib/hecks/bluebook/assembly/marks.rb +227 -0
  73. data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
  74. data/lib/hecks/bluebook/assembly.rb +78 -0
  75. data/lib/hecks/bluebook/attribute.rb +96 -0
  76. data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
  77. data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
  78. data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
  79. data/lib/hecks/bluebook/behaviour/command.rb +116 -0
  80. data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
  81. data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
  82. data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
  83. data/lib/hecks/bluebook/behaviour/lifecycle.rb +51 -0
  84. data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
  85. data/lib/hecks/bluebook/behaviour/process_manager.rb +38 -0
  86. data/lib/hecks/bluebook/behaviour/query.rb +10 -0
  87. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
  88. data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
  89. data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
  90. data/lib/hecks/bluebook/chapter.rb +68 -0
  91. data/lib/hecks/bluebook/command.rb +124 -0
  92. data/lib/hecks/bluebook/domain_port.rb +102 -0
  93. data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
  94. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +906 -0
  95. data/lib/hecks/bluebook/dsl/attribute_collector.rb +327 -0
  96. data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
  97. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1018 -0
  98. data/lib/hecks/bluebook/dsl/command_builder.rb +626 -0
  99. data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
  100. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
  101. data/lib/hecks/bluebook/dsl/entity_builder.rb +305 -0
  102. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
  103. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
  104. data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
  105. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
  106. data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
  107. data/lib/hecks/bluebook/dsl/policy_builder.rb +120 -0
  108. data/lib/hecks/bluebook/dsl/port_builder.rb +30 -0
  109. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
  110. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +207 -0
  111. data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
  112. data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
  113. data/lib/hecks/bluebook/dsl/rule_reference.rb +172 -0
  114. data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
  115. data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
  116. data/lib/hecks/bluebook/dsl/word_gate.rb +221 -0
  117. data/lib/hecks/bluebook/dsl/world_builder.rb +77 -0
  118. data/lib/hecks/bluebook/dsl.rb +45 -0
  119. data/lib/hecks/bluebook/entity.rb +103 -0
  120. data/lib/hecks/bluebook/expression/canonical_form.rb +55 -0
  121. data/lib/hecks/bluebook/expression/evaluator.rb +261 -0
  122. data/lib/hecks/bluebook/expression/projection.json +170 -0
  123. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
  124. data/lib/hecks/bluebook/expression/resolver.rb +630 -0
  125. data/lib/hecks/bluebook/expression.rb +13 -0
  126. data/lib/hecks/bluebook/hexagon.rb +60 -0
  127. data/lib/hecks/bluebook/lifecycle.rb +42 -0
  128. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
  129. data/lib/hecks/bluebook/meta_validator/judge.rb +559 -0
  130. data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
  131. data/lib/hecks/bluebook/meta_validator/port_judge.rb +47 -0
  132. data/lib/hecks/bluebook/meta_validator/readings.rb +350 -0
  133. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +312 -0
  134. data/lib/hecks/bluebook/meta_validator/shapes.rb +246 -0
  135. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
  136. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
  137. data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
  138. data/lib/hecks/bluebook/meta_validator.rb +452 -0
  139. data/lib/hecks/bluebook/model_check.rb +318 -0
  140. data/lib/hecks/bluebook/pattern_subset.rb +120 -0
  141. data/lib/hecks/bluebook/policy.rb +41 -0
  142. data/lib/hecks/bluebook/process_manager.rb +86 -0
  143. data/lib/hecks/bluebook/project_discovery.rb +30 -0
  144. data/lib/hecks/bluebook/project_loader.rb +40 -0
  145. data/lib/hecks/bluebook/project_register.rb +82 -0
  146. data/lib/hecks/bluebook/query.rb +61 -0
  147. data/lib/hecks/bluebook/read_model.rb +109 -0
  148. data/lib/hecks/bluebook/reference.rb +74 -0
  149. data/lib/hecks/bluebook/smoke_test.rb +166 -0
  150. data/lib/hecks/bluebook/synthesizer.rb +95 -0
  151. data/lib/hecks/bluebook/translation.rb +92 -0
  152. data/lib/hecks/bluebook/value_object.rb +58 -0
  153. data/lib/hecks/bluebook.rb +74 -0
  154. data/lib/hecks/codemod.rb +342 -0
  155. data/lib/hecks/construct.rb +71 -0
  156. data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
  157. data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
  158. data/lib/hecks/deploy/oidc.json +18 -0
  159. data/lib/hecks/doc/reference.rb +389 -0
  160. data/lib/hecks/embryonaut_bluebook.rb +75 -0
  161. data/lib/hecks/facade/cli_door.rb +116 -0
  162. data/lib/hecks/facade/cli_runner.rb +190 -0
  163. data/lib/hecks/facade/command_request.rb +105 -0
  164. data/lib/hecks/facade/handle.rb +173 -0
  165. data/lib/hecks/facade/json_door.rb +154 -0
  166. data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
  167. data/lib/hecks/facade/surface/chapter.rb +107 -0
  168. data/lib/hecks/facade/surface.rb +48 -0
  169. data/lib/hecks/facade.rb +44 -0
  170. data/lib/hecks/forms/app.rb +300 -0
  171. data/lib/hecks/forms/command_form_renderer.rb +113 -0
  172. data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
  173. data/lib/hecks/forms/field_renderer.rb +170 -0
  174. data/lib/hecks/forms/field_shape.rb +232 -0
  175. data/lib/hecks/forms/html.rb +53 -0
  176. data/lib/hecks/forms/index_renderer.rb +35 -0
  177. data/lib/hecks/forms/page.rb +157 -0
  178. data/lib/hecks/forms/params.rb +132 -0
  179. data/lib/hecks/forms/query_form_renderer.rb +114 -0
  180. data/lib/hecks/forms/record_renderer.rb +115 -0
  181. data/lib/hecks/forms/record_table.rb +63 -0
  182. data/lib/hecks/forms/reference_options.rb +30 -0
  183. data/lib/hecks/forms/value_object_shape.rb +46 -0
  184. data/lib/hecks/forms.rb +54 -0
  185. data/lib/hecks/fqn.rb +94 -0
  186. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
  187. data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
  188. data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
  189. data/lib/hecks/framework/bluebook/governance.bluebook +130 -0
  190. data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
  191. data/lib/hecks/framework/oidc.json +39 -0
  192. data/lib/hecks/framework.rb +90 -0
  193. data/lib/hecks/freezer.rb +67 -0
  194. data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
  195. data/lib/hecks/fuzzing/isolated_boot.rb +92 -0
  196. data/lib/hecks/fuzzing/properties.rb +1146 -0
  197. data/lib/hecks/fuzzing/replay.rb +635 -0
  198. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
  199. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +54 -0
  200. data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
  201. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +185 -0
  202. data/lib/hecks/fuzzing/sequence_generator.rb +130 -0
  203. data/lib/hecks/fuzzing/value_generator.rb +177 -0
  204. data/lib/hecks/fuzzing.rb +12 -0
  205. data/lib/hecks/grammar/evolve.rb +291 -0
  206. data/lib/hecks/grammar/expression.bluebook +417 -0
  207. data/lib/hecks/grammar/expression_operators.json +1152 -0
  208. data/lib/hecks/grammar/grammar.hecksagon +20 -0
  209. data/lib/hecks/grammar/oidc.json +69 -0
  210. data/lib/hecks/grammar/translation.bluebook +203 -0
  211. data/lib/hecks/grammar.rb +178 -0
  212. data/lib/hecks/ir.rb +126 -0
  213. data/lib/hecks/language/adapter.bluebook +116 -0
  214. data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
  215. data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
  216. data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
  217. data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
  218. data/lib/hecks/language/bluebook/command.bluebook +437 -0
  219. data/lib/hecks/language/bluebook/entity.bluebook +316 -0
  220. data/lib/hecks/language/bluebook/policy.bluebook +172 -0
  221. data/lib/hecks/language/bluebook/process_manager.bluebook +264 -0
  222. data/lib/hecks/language/bluebook/projection.bluebook +267 -0
  223. data/lib/hecks/language/bluebook/query.bluebook +245 -0
  224. data/lib/hecks/language/bluebook/shape.bluebook +292 -0
  225. data/lib/hecks/language/bluebook/syntax.bluebook +447 -0
  226. data/lib/hecks/language/bluebook/vocabulary.bluebook +364 -0
  227. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
  228. data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
  229. data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
  230. data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
  231. data/lib/hecks/language/oidc.json +313 -0
  232. data/lib/hecks/language/port.bluebook +92 -0
  233. data/lib/hecks/language/translation/translation.bluebook +110 -0
  234. data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
  235. data/lib/hecks/language/world/wiring.bluebook +62 -0
  236. data/lib/hecks/language/world/world.bluebook +84 -0
  237. data/lib/hecks/literal.rb +125 -0
  238. data/lib/hecks/naming.rb +121 -0
  239. data/lib/hecks/ports/access_control.port +4 -0
  240. data/lib/hecks/ports/access_control.rb +62 -0
  241. data/lib/hecks/ports/agent/answers.rb +104 -0
  242. data/lib/hecks/ports/agent.port +4 -0
  243. data/lib/hecks/ports/agent.rb +167 -0
  244. data/lib/hecks/ports/authentication.port +4 -0
  245. data/lib/hecks/ports/authentication.rb +50 -0
  246. data/lib/hecks/ports/authorization.port +4 -0
  247. data/lib/hecks/ports/authorization.rb +59 -0
  248. data/lib/hecks/ports/clock.port +4 -0
  249. data/lib/hecks/ports/clock.rb +62 -0
  250. data/lib/hecks/ports/extraction.port +4 -0
  251. data/lib/hecks/ports/extraction.rb +37 -0
  252. data/lib/hecks/ports/identity_assignment.port +4 -0
  253. data/lib/hecks/ports/identity_assignment.rb +45 -0
  254. data/lib/hecks/ports/identity_generation.port +4 -0
  255. data/lib/hecks/ports/identity_generation.rb +49 -0
  256. data/lib/hecks/ports/identity_resolution.port +4 -0
  257. data/lib/hecks/ports/identity_resolution.rb +40 -0
  258. data/lib/hecks/ports/loading.port +4 -0
  259. data/lib/hecks/ports/loading.rb +13 -0
  260. data/lib/hecks/ports/persistence/append_only.rb +102 -0
  261. data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
  262. data/lib/hecks/ports/persistence/execution.rb +18 -0
  263. data/lib/hecks/ports/persistence/lineage.rb +277 -0
  264. data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
  265. data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
  266. data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
  267. data/lib/hecks/ports/persistence.port +4 -0
  268. data/lib/hecks/ports/persistence.rb +39 -0
  269. data/lib/hecks/ports/projection.port +4 -0
  270. data/lib/hecks/ports/projection.rb +76 -0
  271. data/lib/hecks/ports/query/in_memory.rb +60 -0
  272. data/lib/hecks/ports/query/ordering.rb +41 -0
  273. data/lib/hecks/ports/query.rb +36 -0
  274. data/lib/hecks/ports.rb +27 -0
  275. data/lib/hecks/projections/diagrams.rb +413 -0
  276. data/lib/hecks/projections/ir.rb +18 -0
  277. data/lib/hecks/projections/model/deviations.rb +98 -0
  278. data/lib/hecks/projections/model.rb +145 -0
  279. data/lib/hecks/projections/oidc.rb +110 -0
  280. data/lib/hecks/projections/parser_table.rb +159 -0
  281. data/lib/hecks/projections/reference.rb +38 -0
  282. data/lib/hecks/projections/shape.rb +31 -0
  283. data/lib/hecks/projections/statements.rb +110 -0
  284. data/lib/hecks/projections/vocabulary.rb +100 -0
  285. data/lib/hecks/projections.rb +32 -0
  286. data/lib/hecks/projector/cli_projector.rb +437 -0
  287. data/lib/hecks/projector/docs_projector.rb +321 -0
  288. data/lib/hecks/projector/exporter.rb +141 -0
  289. data/lib/hecks/projector/ir_projector.rb +18 -0
  290. data/lib/hecks/projector/narrate_projector.rb +243 -0
  291. data/lib/hecks/projector/target.rb +97 -0
  292. data/lib/hecks/projector.rb +186 -0
  293. data/lib/hecks/query_ir.rb +392 -0
  294. data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
  295. data/lib/hecks/query_specification/common/comparators.rb +25 -0
  296. data/lib/hecks/query_specification/common/comparison.rb +174 -0
  297. data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
  298. data/lib/hecks/query_specification/common/dsl.rb +58 -0
  299. data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
  300. data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
  301. data/lib/hecks/query_specification/common/null_policy.rb +80 -0
  302. data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
  303. data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
  304. data/lib/hecks/query_specification/common/options.rb +32 -0
  305. data/lib/hecks/query_specification/common/order_by.rb +9 -0
  306. data/lib/hecks/query_specification/common/specification.rb +10 -0
  307. data/lib/hecks/query_specification/common/where_clause.rb +9 -0
  308. data/lib/hecks/query_specification/field_path.rb +89 -0
  309. data/lib/hecks/query_specification/hop_path.rb +130 -0
  310. data/lib/hecks/query_specification/read_model/specification.rb +18 -0
  311. data/lib/hecks/query_specification.rb +14 -0
  312. data/lib/hecks/rendering.rb +48 -0
  313. data/lib/hecks/router/namespace_installer.rb +157 -0
  314. data/lib/hecks/router.rb +70 -0
  315. data/lib/hecks/runtime/caller.rb +50 -0
  316. data/lib/hecks/runtime/capability_graph.rb +44 -0
  317. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +124 -0
  318. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +248 -0
  319. data/lib/hecks/runtime/command_interpreter.rb +413 -0
  320. data/lib/hecks/runtime/command_rules/admissibility.rb +307 -0
  321. data/lib/hecks/runtime/command_rules/arithmetic.rb +254 -0
  322. data/lib/hecks/runtime/command_rules/authorization.rb +64 -0
  323. data/lib/hecks/runtime/command_rules/emission.rb +34 -0
  324. data/lib/hecks/runtime/command_rules/references.rb +181 -0
  325. data/lib/hecks/runtime/command_rules.rb +28 -0
  326. data/lib/hecks/runtime/dependency_planning.rb +231 -0
  327. data/lib/hecks/runtime/dispatcher.rb +286 -0
  328. data/lib/hecks/runtime/entity_element.rb +253 -0
  329. data/lib/hecks/runtime/entity_interpreter.rb +228 -0
  330. data/lib/hecks/runtime/era_check.rb +136 -0
  331. data/lib/hecks/runtime/era_guard/shape_diff.rb +128 -0
  332. data/lib/hecks/runtime/era_guard.rb +193 -0
  333. data/lib/hecks/runtime/era_tamper.rb +61 -0
  334. data/lib/hecks/runtime/errors.rb +100 -0
  335. data/lib/hecks/runtime/event.rb +51 -0
  336. data/lib/hecks/runtime/identity.rb +128 -0
  337. data/lib/hecks/runtime/instance.rb +120 -0
  338. data/lib/hecks/runtime/interpreting.rb +69 -0
  339. data/lib/hecks/runtime/loader.rb +143 -0
  340. data/lib/hecks/runtime/policy_interpreter.rb +366 -0
  341. data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
  342. data/lib/hecks/runtime/query_interpreter.rb +254 -0
  343. data/lib/hecks/runtime/reaction_invocation.rb +245 -0
  344. data/lib/hecks/runtime/read_model_interpreter.rb +266 -0
  345. data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
  346. data/lib/hecks/runtime/reference_hop.rb +99 -0
  347. data/lib/hecks/runtime/refusal_wording.rb +124 -0
  348. data/lib/hecks/runtime/registry/saga_persistence.rb +70 -0
  349. data/lib/hecks/runtime/registry/verification.rb +158 -0
  350. data/lib/hecks/runtime/registry.rb +218 -0
  351. data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
  352. data/lib/hecks/runtime/routing.rb +96 -0
  353. data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
  354. data/lib/hecks/runtime/saga_interpreter.rb +301 -0
  355. data/lib/hecks/runtime/storage_shape.rb +120 -0
  356. data/lib/hecks/runtime/tenant_check.rb +84 -0
  357. data/lib/hecks/runtime/tenant_scope.rb +55 -0
  358. data/lib/hecks/runtime/value/admission.rb +112 -0
  359. data/lib/hecks/runtime/value/coercion.rb +500 -0
  360. data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
  361. data/lib/hecks/runtime/value.rb +125 -0
  362. data/lib/hecks/runtime.rb +95 -0
  363. data/lib/hecks/translation/audit/approval_digest.rb +31 -0
  364. data/lib/hecks/translation/audit/layer_one.rb +37 -0
  365. data/lib/hecks/translation/audit/layer_two.rb +77 -0
  366. data/lib/hecks/translation/audit/unfed_report.rb +42 -0
  367. data/lib/hecks/translation/audit.rb +70 -0
  368. data/lib/hecks/translation/reattest.rb +72 -0
  369. data/lib/hecks/translation/rule_compiler.rb +120 -0
  370. data/lib/hecks/translation/scaffold/differ.rb +183 -0
  371. data/lib/hecks/translation/scaffold/renderer.rb +41 -0
  372. data/lib/hecks/translation/scaffold/writer.rb +28 -0
  373. data/lib/hecks/translation/scaffold.rb +30 -0
  374. data/lib/hecks/translation.rb +11 -0
  375. data/lib/hecks/version.rb +16 -0
  376. data/lib/hecks/vocabulary.rb +213 -0
  377. data/lib/hecks.rb +118 -6
  378. metadata +401 -222
  379. data/bin/hecks +0 -7
  380. data/bin/hecks-package +0 -65
  381. data/bin/hecks_console +0 -12
  382. data/bin/hecks_serverless +0 -6
  383. data/lib/cli/build.rb +0 -14
  384. data/lib/cli/command_runner.rb +0 -28
  385. data/lib/cli/console.rb +0 -10
  386. data/lib/cli/generate.rb +0 -37
  387. data/lib/cli/hecks-cli.rb +0 -27
  388. data/lib/cli/test.rb +0 -57
  389. data/lib/console/commands.rb +0 -8
  390. data/lib/console/hecks-console.rb +0 -1
  391. data/lib/packager/README.md +0 -0
  392. data/lib/packager/app_runner.rb +0 -21
  393. data/lib/packager/args.rb +0 -26
  394. data/lib/packager/compatibility/fixnum.rb +0 -6
  395. data/lib/packager/hecks.rb +0 -39
  396. data/lib/packager/query_runner.rb +0 -21
  397. data/lib/packager/resources/Dockerfile +0 -11
  398. data/lib/packager/resources/app_binary +0 -7
  399. data/lib/packager/resources/bundle_config +0 -3
  400. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
  401. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
  402. data/lib/serverless/Domain +0 -32
  403. data/lib/serverless/cli.rb +0 -75
  404. data/lib/serverless/resources/command_name.js +0 -5
  405. data/lib/serverless/resources/environment.js +0 -7
  406. data/lib/serverless/resources/handler.js.tt +0 -28
  407. data/lib/serverless/resources/run_binary.js +0 -22
  408. data/lib/serverless/resources/serverless.yml +0 -20
@@ -0,0 +1,413 @@
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
+ # in one place
52
+ #
53
+ # CONSTRUCT NAMES (aggregate/entity/command/event) ARE USED BARE,
54
+ # UNSANITIZED, as Mermaid node/entity ids — safe because this
55
+ # language's own word grammar only ever admits simple CamelCase/
56
+ # snake_case identifiers there (confirmed: no space or punctuation
57
+ # appears in any real aggregate/command/event name across the corpus
58
+ # this projects from). A `role:` STRING IS FREE TEXT, though — the
59
+ # real corpus already has "Back office"/"Vault officer"/"Branch
60
+ # clerk" — so `roles.mmd` is the one diagram here that sanitizes a
61
+ # name into an id (`role_id`) while keeping the real string as the
62
+ # node's own displayed label.
63
+ module Diagrams
64
+ extend Hecks::Projector::Target
65
+
66
+ projects_as :diagrams, emits: :files
67
+
68
+ module_function
69
+
70
+ def call(bluebook:, options: {})
71
+ files = {}
72
+
73
+ holders_with_lifecycle(bluebook).each do |holder|
74
+ files["#{holder.hecks_name}_lifecycle.mmd"] = lifecycle_diagram(bluebook, holder)
75
+ end
76
+
77
+ if (diagram = relationship_diagram(bluebook))
78
+ files["relationships.mmd"] = diagram
79
+ end
80
+
81
+ if (diagram = dispatch_diagram(bluebook))
82
+ files["dispatch.mmd"] = diagram
83
+ end
84
+
85
+ if (diagram = roles_diagram(bluebook))
86
+ files["roles.mmd"] = diagram
87
+ end
88
+
89
+ if (diagram = ports_diagram(bluebook))
90
+ files["ports.mmd"] = diagram
91
+ end
92
+
93
+ if (diagram = read_model_diagram(bluebook))
94
+ files["read_models.mmd"] = diagram
95
+ end
96
+
97
+ holders(bluebook).each do |holder|
98
+ next if holder.commands.empty? && holder.queries.empty?
99
+
100
+ files["#{holder.hecks_name}_surface.mmd"] = surface_diagram(bluebook, holder)
101
+ end
102
+
103
+ files
104
+ end
105
+
106
+ # ── shared ────────────────────────────────────────────────────────
107
+
108
+ # AN ENTITY CAN CARRY ITS OWN LIFECYCLE, RELATIONSHIP, OR COMMAND
109
+ # TOO — its own `lifecycle`/`reference_to`/`command` block,
110
+ # addressed through its holding aggregate the same way
111
+ # `DocsProjector` already treats an aggregate and its entities
112
+ # alike. Walking both here means a domain's entity gaining any of
113
+ # these needs no change to this file.
114
+ def holders(bluebook)
115
+ bluebook.aggregates.flat_map { |aggregate| [aggregate, *aggregate.entities] }
116
+ end
117
+
118
+ def holders_with_lifecycle(bluebook) = holders(bluebook).select(&:lifecycle)
119
+
120
+ # `chapter_name` DRIVES THE RE-RUN HINT ALWAYS — that's the one
121
+ # argument `bin/project_diagrams` actually takes, regardless of
122
+ # which single aggregate/entity `subject` happens to name. Passing
123
+ # the wrong one here once already produced a real, committed
124
+ # `Order_lifecycle.mmd` telling a reader to run
125
+ # `bin/project_diagrams <domain-path> Order` — a chapter name
126
+ # Hecks.boot has never heard of.
127
+ def header(chapter_name, subject)
128
+ <<~HEADER
129
+ %% GENERATED by bin/project_diagrams from #{subject} — DO NOT EDIT BY HAND.
130
+ %% Re-run `bin/project_diagrams <domain-path> #{chapter_name}` after any change.
131
+ HEADER
132
+ end
133
+
134
+ # ── lifecycle -> stateDiagram-v2 ─────────────────────────────────
135
+
136
+ def lifecycle_diagram(bluebook, holder)
137
+ lifecycle = holder.lifecycle
138
+ edges = lifecycle.transitions.flat_map do |command_name, transition|
139
+ Array(transition.from).map { |from_state| " #{from_state} --> #{transition.target}: #{command_name}" }
140
+ end
141
+
142
+ subject = "#{holder.hecks_name}'s own declared lifecycle (field: #{lifecycle.field})"
143
+ <<~MERMAID
144
+ #{header(bluebook.name, subject)}stateDiagram-v2
145
+ [*] --> #{lifecycle.default}
146
+ #{edges.join("\n")}
147
+ MERMAID
148
+ end
149
+
150
+ # ── relationships -> erDiagram ───────────────────────────────────
151
+
152
+ # STANDARD CROW'S-FOOT READING, the same convention every ORM's own
153
+ # ERD generator (Rails' erd gem included) already uses:
154
+ # `has_many`/`has_one` are read from the OWNING side — one Holder
155
+ # relates to many/one Target. `belongs_to`/`reference_to` are read
156
+ # from the TARGET's side instead — one Target can be pointed at by
157
+ # MANY Holders — because a bare reference carries no promise about
158
+ # how many holders point back at it; "many" is the honest default
159
+ # absent a declared uniqueness rule this language doesn't expose.
160
+ # `optional?` only ever softens the side that can genuinely be
161
+ # absent (a nilable reference, an empty has_one) — never the
162
+ # crow's-foot "many" marker, which is a structural fact independent
163
+ # of any one instance's optionality.
164
+ def relationship_diagram(bluebook)
165
+ edges = holders(bluebook).flat_map do |holder|
166
+ holder.attributes.select(&:reference?).map { |attribute| relationship_edge(holder, attribute) }
167
+ end
168
+ return nil if edges.empty?
169
+
170
+ subject = "#{bluebook.name}'s own declared reference_to/belongs_to/has_many/has_one attributes"
171
+ "#{header(bluebook.name, subject)}erDiagram\n#{edges.join("\n")}\n"
172
+ end
173
+
174
+ def relationship_edge(holder, attribute)
175
+ target = attribute.type.target_name
176
+ case attribute.relationship
177
+ when "has_many"
178
+ %( #{holder.hecks_name} ||--o{ #{target} : "#{attribute.name}")
179
+ when "has_one"
180
+ %( #{holder.hecks_name} ||--#{attribute.optional? ? 'o|' : '||'} #{target} : "#{attribute.name}")
181
+ when "belongs_to", "reference_to"
182
+ %( #{target} #{attribute.optional? ? '|o' : '||'}--o{ #{holder.hecks_name} : "#{attribute.name}")
183
+ end
184
+ end
185
+
186
+ # ── dispatch -> flowchart ─────────────────────────────────────────
187
+
188
+ # A COMMAND NODE, STADIUM-SHAPED (`(["..."])`); AN EVENT NODE,
189
+ # HEXAGONAL (`{{"..."}}`) — one visual vocabulary for "a thing
190
+ # someone does" versus "a fact that happened", matching the
191
+ # language's own verb/event distinction. Command ids are qualified
192
+ # by their owning aggregate (`cmd_Order_Purchase`) since two
193
+ # aggregates may share a command name; event ids are bare
194
+ # (`evt_PizzaCreated`) since an event is this domain's own
195
+ # addressing key, the same way `policy.on_event` reaches it.
196
+ def dispatch_diagram(bluebook)
197
+ lines = []
198
+
199
+ holders(bluebook).each do |holder|
200
+ holder.commands.each do |command|
201
+ command.emits.each { |event| lines << emits_edge(holder, command, event) }
202
+ end
203
+ end
204
+
205
+ bluebook.policies.each { |policy| lines << trigger_edge(policy) }
206
+
207
+ lines.compact!
208
+ return nil if lines.empty?
209
+
210
+ subject = "#{bluebook.name}'s own declared commands' emits and policies' on/trigger"
211
+ "#{header(bluebook.name, subject)}flowchart LR\n#{lines.uniq.join("\n")}\n"
212
+ end
213
+
214
+ def emits_edge(holder, command, event)
215
+ %( #{command_node(holder.hecks_name, command.hecks_name)} -->|emits| #{event_node(event)})
216
+ end
217
+
218
+ # `on_event` IS SOMETIMES AGGREGATE-QUALIFIED
219
+ # (`"Account.AccountFrozen"`) AND SOMETIMES BARE
220
+ # (`"CustomerSuspended"`) in the real corpus — `emits` never is,
221
+ # so this always matches against the bare tail, the same
222
+ # normalization a reader has to do by eye today.
223
+ #
224
+ # A TRIGGER CROSSING INTO ANOTHER DOMAIN (`policy.target_domain`)
225
+ # still draws — the target command just has no incoming `emits`
226
+ # edge of its own here, which honestly shows "dispatch continues
227
+ # elsewhere" rather than silently dropping the edge. The label
228
+ # names which domain, so that's not a dead end on the page either.
229
+ def trigger_edge(policy)
230
+ bare_event = policy.on_event.to_s.split(".").last
231
+ aggregate_name, command_name = policy.trigger_command.to_s.split(".", 2)
232
+ label = policy.target_domain ? "triggers in #{policy.target_domain}" : "triggers"
233
+ %( #{event_node(bare_event)} -->|#{label}| #{command_node(aggregate_name, command_name)})
234
+ end
235
+
236
+ def command_node(aggregate_name, command_name)
237
+ %(cmd_#{aggregate_name}_#{command_name}(["#{aggregate_name}.#{command_name}"]))
238
+ end
239
+
240
+ def event_node(event_name) = %(evt_#{event_name}{{"#{event_name}"}})
241
+
242
+ # ── roles -> flowchart ────────────────────────────────────────────
243
+
244
+ # WHO ISSUES WHAT, ACROSS THE WHOLE DOMAIN — data no existing
245
+ # projection draws at all today (the reference pages' own
246
+ # `command_entry` only ever prints a command's role as a single
247
+ # line of prose, never assembled across commands). A command with
248
+ # no declared `role` draws nothing — there is no fact to state.
249
+ # Circle-shaped so a role reads as "who" beside `dispatch.mmd`'s
250
+ # stadium ("what someone does") and hexagon ("what happened").
251
+ def roles_diagram(bluebook)
252
+ lines = holders(bluebook).flat_map do |holder|
253
+ holder.commands.select(&:role).map { |command| role_edge(holder, command) }
254
+ end
255
+ return nil if lines.empty?
256
+
257
+ subject = "#{bluebook.name}'s own declared command roles"
258
+ "#{header(bluebook.name, subject)}flowchart LR\n#{lines.uniq.join("\n")}\n"
259
+ end
260
+
261
+ def role_edge(holder, command)
262
+ %( #{role_node(command.role)} -->|issues| #{command_node(holder.hecks_name, command.hecks_name)})
263
+ end
264
+
265
+ def role_node(role_name) = %(#{role_id(role_name)}((#{role_name})))
266
+
267
+ # A ROLE NAME IS FREE TEXT ("Back office", "Vault officer") —
268
+ # unlike every other name this file uses as a bare id, this one
269
+ # has to be sanitized to become a legal Mermaid identifier. The
270
+ # real string still appears as the node's own label
271
+ # (`role_node`); only the id is mangled.
272
+ def role_id(role_name) = "role_#{role_name.to_s.gsub(/[^A-Za-z0-9]+/, '_')}"
273
+
274
+ # ── ports -> flowchart ───────────────────────────────────────────
275
+
276
+ # A PORT OPERATION IS A BOUNDARY TRANSLATION, NOT A VERB OR A FACT —
277
+ # its own reference page says so plainly ("the builder behind it
278
+ # defines no `given` or `sets`, so an operation cannot read
279
+ # aggregate state or mutate a record itself"), so it gets a third
280
+ # shape, a trapezoid, beside `dispatch.mmd`'s stadium/hexagon
281
+ # vocabulary. An aggregate drawn as a `to:` target is a cylinder —
282
+ # state landing somewhere, the same reason a data store gets one
283
+ # in an ordinary flowchart.
284
+ #
285
+ # TWO EDGE KINDS PER OPERATION: a dotted "exposes" edge from the
286
+ # aggregate the port hangs off (always present — a port always
287
+ # belongs to exactly one aggregate), and a solid "to:" edge to
288
+ # whichever aggregate the operation itself names as its receiver
289
+ # (present only when `to:` is declared — PR #351's own real
290
+ # addition; before it, this data didn't exist to draw at all).
291
+ # `emits` reuses `dispatch.mmd`'s own `event_node` unchanged — the
292
+ # same fact, reached from a different direction.
293
+ #
294
+ # `bluebook.aggregates`, NOT the shared `holders` — unlike a
295
+ # lifecycle/relationship/command, a port belongs to an AGGREGATE
296
+ # only; an entity has no `ports` method at all (confirmed: calling
297
+ # it raises, it isn't just always empty), so walking entities here
298
+ # the way every other diagram in this file does would crash on
299
+ # the first entity-bearing domain.
300
+ def ports_diagram(bluebook)
301
+ lines = bluebook.aggregates.flat_map do |holder|
302
+ holder.ports.flat_map { |port| port.operations.map { |operation| port_edges(holder, port, operation) } }
303
+ end.flatten
304
+
305
+ return nil if lines.empty?
306
+
307
+ subject = "#{bluebook.name}'s own declared port operations (which aggregate exposes each, its to:, and its emits)"
308
+ "#{header(bluebook.name, subject)}flowchart LR\n#{lines.uniq.join("\n")}\n"
309
+ end
310
+
311
+ def port_edges(holder, port, operation)
312
+ op = port_operation_node(holder.hecks_name, port.name, operation.hecks_name)
313
+ edges = [" #{holder.hecks_name}[(#{holder.hecks_name})] -.->|exposes| #{op}"]
314
+ edges << " #{op} -->|to: #{operation.to}| #{operation.to}[(#{operation.to})]" if operation.to
315
+ operation.emits.each { |event| edges << " #{op} -->|emits| #{event_node(event)}" }
316
+ edges
317
+ end
318
+
319
+ def port_operation_node(aggregate_name, port_name, operation_name)
320
+ id = "op_#{aggregate_name}_#{port_name}_#{operation_name}"
321
+ %(#{id}[/"#{port_name}.#{operation_name}"/])
322
+ end
323
+
324
+ # ── read models -> flowchart ─────────────────────────────────────
325
+
326
+ # THE READ-SIDE COMPLEMENT TO `relationships.mmd` — that diagram
327
+ # shows how aggregates reference each other for WRITES
328
+ # (`has_many`/`belongs_to`/`reference_to`); this shows how a
329
+ # `read_model` ASSEMBLES data for READS, from
330
+ # `aggregate_heads` — the same list `where`/`group_by`/`order_by`
331
+ # all operate over, and the one fact every read_model has
332
+ # regardless of whether it's rooted (`reference_target`) or
333
+ # gathers heads with no root at all (a rootless read model, real
334
+ # in the corpus: `AccountsByKind`).
335
+ #
336
+ # A READ MODEL IS A SUBROUTINE SHAPE (`[[...]]`, "a predefined
337
+ # process") — a fourth shape, beside `ports.mmd`'s trapezoid and
338
+ # `dispatch.mmd`'s stadium/hexagon: not a verb, not a fact, not a
339
+ # boundary translation, but a standing, reusable view. Every
340
+ # aggregate it draws from is a cylinder — the same "state lands
341
+ # somewhere" shape `ports.mmd`'s `to:` target already uses, and
342
+ # the same bare id, so an aggregate feeding several read_models
343
+ # (real in banking: `Account` feeds four) merges into one node
344
+ # across the whole diagram.
345
+ #
346
+ # THE LABEL NAMES THE SHAPE OF THE ANSWER, NOT JUST THE NAME —
347
+ # `(count)`/`(median: field)` for the two real aggregations in the
348
+ # corpus, nothing appended for an ordinary row-returning
349
+ # read_model. Still MVP scope: `where`/`group_by`/`order_by`
350
+ # aren't drawn at all yet — real facts, not invented, just not
351
+ # this diagram's job yet.
352
+ def read_model_diagram(bluebook)
353
+ lines = bluebook.read_models.flat_map { |read_model| read_model_edges(read_model) }
354
+ return nil if lines.empty?
355
+
356
+ subject = "#{bluebook.name}'s own declared read_models and the aggregates each is assembled from"
357
+ "#{header(bluebook.name, subject)}flowchart LR\n#{lines.uniq.join("\n")}\n"
358
+ end
359
+
360
+ def read_model_edges(read_model)
361
+ shape = read_model.to_h
362
+ node = %(rm_#{shape[:name]}[["#{read_model_label(shape)}"]])
363
+ Array(shape[:aggregate_heads]).map do |head|
364
+ # QUOTED, NOT BARE — an edge label containing `[` or `]`
365
+ # (`accounts[]`, marking the "many" side) breaks Mermaid's own
366
+ # `|label|` parser outright if left unquoted: it reads the
367
+ # `[` as the START OF A NEW NODE SHAPE mid-label, not text.
368
+ # Confirmed live against the real parser before this quoting
369
+ # existed — every OTHER edge label in this file happens to be
370
+ # a bare word or already-quoted string, so this is the one
371
+ # spot that needed it.
372
+ label = head[:many] ? "#{head[:as]}[]" : head[:as]
373
+ %( #{head[:aggregate]}[(#{head[:aggregate]})] -->|"#{label}"| #{node})
374
+ end
375
+ end
376
+
377
+ def read_model_label(shape)
378
+ return "#{shape[:name]} (count)" if shape[:count]
379
+ return "#{shape[:name]} (median: #{shape[:median_field]})" if shape[:median_field]
380
+
381
+ shape[:name]
382
+ end
383
+
384
+ # ── surface -> flowchart ─────────────────────────────────────────
385
+
386
+ # "WHAT CAN I DO TO THIS, WHAT CAN I ASK ABOUT IT" — one file per
387
+ # holder, unlike every other diagram here: `dispatch.mmd` already
388
+ # shows a command's own onward reaction chain, but never an
389
+ # aggregate's own FULL command/query menu in one place, and
390
+ # `roles.mmd` shows who issues a command without saying what else
391
+ # that same aggregate answers. This is the one diagram meant to
392
+ # be read starting from the aggregate, not from a verb or a fact.
393
+ #
394
+ # A QUERY IS A DIAMOND — a fifth shape, beside `dispatch.mmd`'s
395
+ # stadium/hexagon, `ports.mmd`'s trapezoid, and `read_models.mmd`'s
396
+ # subroutine: a question with an answer, not a verb that changes
397
+ # anything. Command edges are solid ("does"); query edges are
398
+ # dotted ("asks") — the same solid/dotted split `ports.mmd`
399
+ # already uses for "routes to:" versus "exposes".
400
+ def surface_diagram(bluebook, holder)
401
+ lines = holder.commands.map { |command| " #{holder.hecks_name}[(#{holder.hecks_name})] -->|does| #{command_node(holder.hecks_name, command.hecks_name)}" }
402
+ lines += holder.queries.map { |query| " #{holder.hecks_name}[(#{holder.hecks_name})] -.->|asks| #{query_node(holder.hecks_name, query.hecks_name)}" }
403
+
404
+ subject = "#{holder.hecks_name}'s own declared commands and queries"
405
+ "#{header(bluebook.name, subject)}flowchart LR\n#{lines.uniq.join("\n")}\n"
406
+ end
407
+
408
+ def query_node(aggregate_name, query_name)
409
+ %(qry_#{aggregate_name}_#{query_name}{"#{aggregate_name}.#{query_name}"})
410
+ end
411
+ end
412
+ end
413
+ 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
@@ -0,0 +1,98 @@
1
+ module Hecks
2
+ module Projections
3
+ module Model
4
+ # HOW THE MODEL'S SHAPE DIFFERS FROM THE LANGUAGE'S, and why.
5
+ #
6
+ # Every construct's `emits_ir` restates what the grammar declares —
7
+ # and the two legitimately differ, in seven ways. These lived only
8
+ # as prose in Ruby comments until spec/model_shape_conformance_spec
9
+ # made them checkable; they live HERE so the generator and the gate
10
+ # read one source rather than two that must agree.
11
+ #
12
+ # A reason is carried beside each entry because the generated file
13
+ # will CARRY it: a deviation is exactly the kind of thing whose
14
+ # explanation must survive regeneration, and the only way it can is
15
+ # to be emitted rather than typed into the output.
16
+ module Deviations
17
+ # The grammar is relational — a Command points UP at its
18
+ # Aggregate — where the model composes. An explicit `as:` still
19
+ # keeps its `_id` (Command's own `entity_id`, kept as data); the
20
+ # parent link itself mints bare now (ADR 0025) — `aggregate` or
21
+ # `bluebook`, whichever this category's creating command declares
22
+ # first — so both spellings are checked. Entity spells its own
23
+ # (separate, non-colliding) text twin of the parent link `owner`.
24
+ PARENT_REF = ->(field) { field.to_s.match?(/_id\z/) || %i[owner aggregate bluebook].include?(field) }
25
+
26
+ # The judge's own field, never the model's — contracts.rb already
27
+ # says so with `derived: { position: :walk }`.
28
+ JUDGE_ONLY = %i[position].freeze
29
+
30
+ # What the model holds that the grammar declares elsewhere: the
31
+ # containment edges, stated in syntax.bluebook's Keyword rows as
32
+ # `context` -> `opens`.
33
+ CONTAINED = {
34
+ "Bluebook" => %i[aggregates read_models policies process_managers],
35
+ "Aggregate" => %i[commands entities queries value_objects],
36
+ # S17, ADR 0026 — an entity may nest further entities now
37
+ # (`Dispatch`, inside `Handler`) — same containment edge as
38
+ # Aggregate's own `entities`, one level down.
39
+ "Entity" => %i[commands entities queries],
40
+ "ValueObject" => %i[members],
41
+ "ProcessManager" => %i[handlers]
42
+ }.freeze
43
+
44
+ # One model field gathered from several declared ones —
45
+ # contracts.rb's own `[:folded, ...]` shape, as the pair it is.
46
+ FOLDED = {
47
+ "Aggregate" => { lifecycle: %i[state_field state_start transitions] },
48
+ "Entity" => { lifecycle: %i[state_field state_start transitions] },
49
+ "Query" => { order_by: %i[order_field order_way] }
50
+ }.freeze
51
+
52
+ # The inverse of a fold, and it had no name at all: one declared
53
+ # field opening into several the model holds apart.
54
+ UNPACKED = {
55
+ "ReadModel" => { options: %i[wheres order_by limit] }
56
+ }.freeze
57
+
58
+ # Model-only, each for its own reason rather than by oversight.
59
+ COMPUTED = {
60
+ "Bluebook" => { ir_version: "the EMISSION's own version, not the domain's",
61
+ canonical_form: "the normalisation table every reader needs beside the IR" },
62
+ "ValueObject" => { closed_set: "an empty one_of and no one_of are otherwise indistinguishable" },
63
+ "Aggregate" => { ports: "declared in the hecksagon, attached after the aggregate exists" }
64
+ }.freeze
65
+
66
+ # DECLARED, AND DELIBERATELY NOT EMITTED. The category that had no
67
+ # home anywhere before — each of these was a comment and nothing
68
+ # more.
69
+ OFF_THE_WIRE = {
70
+ "Policy" => { aggregate: "the wire format is a pinned contract, and it does not carry " \
71
+ "where a policy was written before the builder hoisted it" },
72
+ "Bluebook" => { formerly_known_as: "a rename's old name is a fact about the source",
73
+ normalisations: "the normalisation table rides on canonical_form instead" },
74
+ "ValueObject" => { rows: "the language's name for a closed set's members; emitted as `members`" }
75
+ }.freeze
76
+
77
+ # Emitted by `to_h`'s own merge rather than by `emits_ir` — the
78
+ # two constructs whose shape is genuinely not fixed, because the
79
+ # query specification layer grew options after them.
80
+ DYNAMIC_TAIL = {
81
+ "Query" => %i[options],
82
+ "ReadModel" => %i[options group_by aggregate_heads count median_field]
83
+ }.freeze
84
+
85
+ module_function
86
+
87
+ # The tables that carry a reason answer with names only when the
88
+ # caller wants the set rather than the explanations.
89
+ def off_the_wire(name) = OFF_THE_WIRE.fetch(name, {}).keys
90
+ def computed(name) = COMPUTED.fetch(name, {}).keys
91
+ def contained(name) = CONTAINED.fetch(name, [])
92
+ def folded(name) = FOLDED.fetch(name, {})
93
+ def unpacked(name) = UNPACKED.fetch(name, {})
94
+ def dynamic_tail(name) = DYNAMIC_TAIL.fetch(name, [])
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,145 @@
1
+ require_relative "model/deviations"
2
+ require_relative "../projector"
3
+
4
+ module Hecks
5
+ module Projections
6
+ # THE MODEL CLASSES, PROJECTED FROM THE LANGUAGE THAT DECLARES THEM.
7
+ #
8
+ # A construct's HOLDING half — its readers, its emission, a
9
+ # constructor that assigns declared fields and hands off — restates
10
+ # what `bluebook.bluebook` already says, three times over in Ruby.
11
+ # This renders it instead.
12
+ #
13
+ # ONLY THE HOLDING HALF. `Behaviour::X` is hand-written and permanent,
14
+ # and `settle` is the seam: everything a declaration cannot state
15
+ # lives behind it, so regenerating can never be lossy. That property
16
+ # was established construct by construct before any of this was
17
+ # written — the chapter looked generatable and was not, until its
18
+ # `@hecks_root`, ports table and child stamping moved behind `settle`.
19
+ #
20
+ # THE HOST MANIFEST IS THE HONEST PART. The grammar states the fields;
21
+ # it does not state which constructs are Ruby CLASSES rather than
22
+ # instances, what a constructor's defaults are, or how a value is
23
+ # coerced on the way in. Those are facts about Ruby, not about
24
+ # bluebooks, so they are declared here rather than pretended into the
25
+ # language — and keeping them in one table is what would let a second
26
+ # host swap this file rather than edit thirteen.
27
+ module Model
28
+ extend Projector::Target
29
+
30
+ projects_as :model, declares: "Bluebook", emits: :files
31
+
32
+ # PER-CONSTRUCT RUBY FACTS. `coerce` is the only fiddly column: a
33
+ # declared field arrives as whatever the builder handed over, and
34
+ # each construct has always normalised its own on the way in.
35
+ HOST = {
36
+ "Policy" => {
37
+ construct: "Policy",
38
+ file: "policy.rb",
39
+ behaviour: "Behaviour::Policy",
40
+ readers: %i[name on_event trigger_command target_domain where for_each with_spec],
41
+ accessors: %i[aggregate],
42
+ defaults: { name: nil, on_event: "nil", trigger_command: "nil",
43
+ target_domain: "nil", where: "nil", for_each: "nil",
44
+ with_spec: "[]", aggregate: "nil" },
45
+ coerce: { name: ".to_s", aggregate: "&.to_s" },
46
+ # A LIST OF BINDINGS IS NOT A SCALAR ON THE WIRE. Every other
47
+ # field emits as itself; this one has to render the way
48
+ # `DispatchSpec`'s own `with_spec` does — keys to strings, and
49
+ # `render_value` KEEPING the leading colon on a Symbol, because
50
+ # a binding that reads an event field and one that supplies a
51
+ # literal string are otherwise indistinguishable once written
52
+ # down (see `MetaValidator::Readings`' own note on exactly that).
53
+ renders: { with_spec: "-> { with_spec.map { |key, value| [key.to_s, Bluebook.render_value(value)] } }" },
54
+ settles: false
55
+ }
56
+ }.freeze
57
+
58
+ module_function
59
+
60
+ def call(bluebook:, options: {})
61
+ HOST.to_h { |name, host| [host.fetch(:file), render(bluebook, name, host)] }
62
+ end
63
+
64
+ def render(bluebook, name, host)
65
+ <<~RUBY
66
+ # GENERATED — projected from the language's own #{name} aggregate.
67
+ # DO NOT EDIT: the holding half is rendered, and #{host.fetch(:behaviour)}
68
+ # is where anything hand-written belongs.
69
+ require_relative "behaviour/#{File.basename(host.fetch(:file), ".rb")}"
70
+
71
+ module Hecks
72
+ module Bluebook
73
+ class #{name}
74
+ include Hecks::IR
75
+ include #{host.fetch(:behaviour)}
76
+
77
+ #{indent(emits(bluebook, name), 6)}
78
+
79
+ #{indent(readers(host), 6)}
80
+
81
+ #{indent(constructor(host), 6)}
82
+ end
83
+ end
84
+ end
85
+ RUBY
86
+ end
87
+
88
+ # The emission, keyed as the model spells it and sourced as the
89
+ # language declares it.
90
+ def emits(bluebook, name)
91
+ fields = emitted_fields(bluebook, name)
92
+ renders = HOST.fetch(name).fetch(:renders, {})
93
+ width = fields.map { |f| f.to_s.length }.max.to_i
94
+
95
+ "emits_ir(\n" +
96
+ fields.map { |f| " #{"#{f}:".ljust(width + 1)} #{renders.fetch(f, ":#{f}")}" }.join(",\n") +
97
+ "\n)"
98
+ end
99
+
100
+ # WHAT THE CONSTRUCT EMITS: what the language declares, less every
101
+ # deviation the tables account for. The generator and
102
+ # spec/model_shape_conformance_spec compute this the same way, from
103
+ # the same tables, which is the point of the tables being in lib.
104
+ def emitted_fields(bluebook, name)
105
+ bluebook.aggregate(name).attributes.map(&:name)
106
+ .reject { |f| Deviations::PARENT_REF.call(f) } -
107
+ Deviations::JUDGE_ONLY -
108
+ Deviations.off_the_wire(name) -
109
+ Deviations.dynamic_tail(name) -
110
+ Deviations.folded(name).values.flatten -
111
+ Deviations.unpacked(name).keys
112
+ end
113
+
114
+ def readers(host)
115
+ lines = ["attr_reader #{host.fetch(:readers).map { |r| ":#{r}" }.join(', ')}"]
116
+ accessors = host.fetch(:accessors, [])
117
+ return lines.join("\n") if accessors.empty?
118
+
119
+ # A declared field the model deliberately does not emit still
120
+ # needs a reader, and the REASON it is off the wire is carried
121
+ # here rather than typed in — a comment that survives
122
+ # regeneration is one the generator writes.
123
+ reasons = Deviations::OFF_THE_WIRE.fetch(host.fetch(:construct, ""), {})
124
+ (lines + accessors.map { |a|
125
+ why = reasons[a]
126
+ (why ? "\n# #{a.upcase}, DECLARED AND DELIBERATELY OFF THE WIRE\n# #{wrap(why)}\n" : "") +
127
+ "attr_accessor :#{a}"
128
+ }).join("\n")
129
+ end
130
+
131
+ def constructor(host)
132
+ args = host.fetch(:defaults).map { |f, d| d ? "#{f}: #{d}" : "#{f}:" }.join(", ")
133
+ body = host.fetch(:defaults).keys.map { |f|
134
+ " @#{f} = #{f}#{host.fetch(:coerce, {})[f]}"
135
+ }
136
+ body << "\n settle" if host.fetch(:settles, true)
137
+
138
+ "def initialize(#{args})\n#{body.join("\n")}\nend"
139
+ end
140
+
141
+ def indent(text, by) = text.lines.map { |l| l.strip.empty? ? l : (" " * by) + l }.join
142
+ def wrap(text) = text.scan(/.{1,62}(?:\s|$)/).map(&:strip).join("\n# ")
143
+ end
144
+ end
145
+ end