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,75 @@
1
+ module Hecks
2
+ # A VENDORED, EXTERNAL BLUEBOOK — same shape as Framework (framework.rb),
3
+ # for members that don't ship inside hecks's own lib/ at all: a
4
+ # separate, independently-versioned package
5
+ # (github.com/chrisyoung/embryonaut_bluebooks) that a consuming project
6
+ # vendors into its OWN checkout, the same way a project already vendors
7
+ # hecks itself (bin/vendor_hecks, vendor/hecks/).
8
+ #
9
+ # RECOVERED, NOT REBUILT — this module and its `uses_embryonaut_bluebook`
10
+ # DSL word (hecksagon_builder.rb) were built on a prior commit of this
11
+ # repo (933d1dd), vendored out to a real consumer (lifeadelics/domain,
12
+ # for embryonaut_bluebooks/payments), and then lost from this repo's own
13
+ # reachable history — a hard reset or rebase left no branch containing
14
+ # that commit. The lifeadelics vendor snapshot (a `git archive` of that
15
+ # commit, committed into their repo) was the only surviving copy; this
16
+ # file is ported forward from it, checked against current `main`'s own
17
+ # conventions rather than copied wholesale, since the two trees had
18
+ # otherwise diverged for weeks in both directions.
19
+ #
20
+ # RESOLVED FROM THE CONSUMING REGISTRY'S OWN ROOT, not this gem's
21
+ # __dir__ — Framework::ROOT can be a fixed, `__dir__`-relative constant
22
+ # because framework members ship inside this gem; an embryonaut bluebook
23
+ # ships inside the CONSUMER's own checkout instead, at
24
+ # `<registry.root>/vendor/embryonaut_bluebooks/<name>/bluebook/`. There
25
+ # is no fixed answer until a registry (and its root) actually exists, so
26
+ # this resolves lazily, per call — the same reason `uses_framework`
27
+ # itself only runs at hecksagon-build time, when a real registry is
28
+ # current.
29
+ #
30
+ # EVERY `.bluebook` FILE IN THE PACKAGE, SORTED — not just one. Unlike a
31
+ # framework member (one file, named by its own stem), a vendored package
32
+ # can span several bluebook files that reopen the SAME `Hecks.bluebook`
33
+ # (embryonaut_bluebooks/payments/bluebook/{payment,payments,policies}
34
+ # .bluebook all reopen "Payments"). Load order matters — policies
35
+ # .bluebook names `Payment::Succeed` and needs the aggregate already
36
+ # built — and a plain alphabetical sort already gives the right order:
37
+ # payment < payments < policies, the same reason that package's own
38
+ # files are named to fall in that order in the first place.
39
+ #
40
+ # ONLY THE BLUEBOOK FILES — same restriction Framework draws, same
41
+ # reason: a `.hecksagon`/`.port`/`.adapter` is a WIRING decision
42
+ # (persistence, which processor adapter is bound) that belongs to
43
+ # whoever is deploying, never baked into the vendored package itself.
44
+ # embryonaut_bluebooks/payments ships its own mock `.hecksagon` for its
45
+ # own spec suite; a consumer declares its OWN separate
46
+ # `Hecks.hecksagon "Payments" do ... end` to bind real storage/adapters
47
+ # — see Framework's own comment for the fuller reasoning, identical here.
48
+ #
49
+ # IDEMPOTENT THE SAME WAY Framework.load! IS — checked against the
50
+ # bluebook this package actually declares (`Naming.pascal("payments")`
51
+ # => "Payments"), not a separate ledger. A vendored package's directory
52
+ # name and its declared `Hecks.bluebook` name are the one convention
53
+ # this reuses from Framework rather than reinventing.
54
+ module EmbryonautBluebook
55
+ def self.load!(name, registry: Hecks.current_registry)
56
+ unless registry&.root
57
+ raise Runtime::WiringError,
58
+ "uses_embryonaut_bluebook(#{name.inspect}) needs a registry with a root to vendor from"
59
+ end
60
+
61
+ return if registry.bluebook(Naming.pascal(name.to_s))
62
+
63
+ dir = File.join(registry.root, "vendor", "embryonaut_bluebooks", name.to_s, "bluebook")
64
+ files = Dir.glob(File.join(dir, "*.bluebook"))
65
+
66
+ if files.empty?
67
+ raise Runtime::WiringError,
68
+ "no vendored embryonaut bluebook named #{name.inspect} at #{dir} — " \
69
+ "run bin/vendor_embryonaut_bluebooks #{name}"
70
+ end
71
+
72
+ files.each { |file| Kernel.load(file) }
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,116 @@
1
+ require_relative "../runtime/errors"
2
+
3
+ module Hecks
4
+ module Facade
5
+ # THE CLI DOOR — WHERE Facade MEETS A CALLER HOLDING FLAT STRINGS.
6
+ #
7
+ # `JsonDoor` beside this one translates for a caller holding parsed JSON:
8
+ # String keys, already-nested objects, real Integers. A command line has
9
+ # neither of those. It has `sequence.value=99` — one flat string, with the
10
+ # nesting spelled as a path and the type not spelled at all.
11
+ #
12
+ # So this does the two things that turns into: rebuild the nesting, and
13
+ # give every leaf the type the chapter declared for it.
14
+ #
15
+ # THE TYPE COMES FROM THE PROJECTION, NEVER FROM THE VALUE. A door that
16
+ # guessed — "99 looks like a number" — would send the Integer 99 for a
17
+ # version string of "99", and be wrong in a way nothing downstream could
18
+ # detect, because both are perfectly good arguments. `Projector::CliProjector`
19
+ # already read the declared field type out of the value object; this only
20
+ # applies it.
21
+ module CliDoor
22
+ module_function
23
+
24
+ # `["reference.value=BUG#1", "sequence.value=99"]` against a projected
25
+ # verb spec -> `{ reference: { value: "BUG#1" }, sequence: { value: 99 } }`
26
+ def arguments(spec, pairs)
27
+ # Legacy options are accepted but not printed in help. This lets an
28
+ # existing id=... aggregate invocation cross the new receiver boundary
29
+ # while the projected surface teaches to=... exclusively.
30
+ options = (spec[:arguments] + Array(spec[:legacy_arguments])).to_h do |argument|
31
+ [argument[:path], argument]
32
+ end
33
+
34
+ pairs.each_with_object({}) do |pair, args|
35
+ path, value = split(pair)
36
+ argument = options[path] || options[expand(path, options)] ||
37
+ raise(Runtime::NotFound, unknown(path, options.keys))
38
+
39
+ full = options.key?(path) ? path : expand(path, options)
40
+ next append(args, full.split("."), cast(value, argument[:type])) if argument[:list]
41
+
42
+ bury(args, full.split("."), cast(value, argument[:type]))
43
+ end
44
+ end
45
+
46
+ def split(pair)
47
+ name, value = pair.split("=", 2)
48
+ raise Runtime::NotFound, "#{pair.inspect} is not name=value" if value.nil?
49
+
50
+ [name, value]
51
+ end
52
+
53
+ # THE SHORT FORM, FOR THE COMMON CASE. Almost every value object in this
54
+ # corpus has exactly one field, so `reference=BUG#1` is unambiguous and
55
+ # is what anybody types. Expanded only when precisely one option starts
56
+ # with that prefix — two would be a guess, and a guess about which field
57
+ # a caller meant is worse than asking them to say.
58
+ def expand(path, options)
59
+ candidates = options.keys.select { |key| key.start_with?("#{path}.") }
60
+ candidates.length == 1 ? candidates.first : path
61
+ end
62
+
63
+ def cast(value, type)
64
+ case type
65
+ when "Integer" then Integer(value)
66
+ when "Float" then Float(value)
67
+ when "Boolean" then %w[true yes 1].include?(value.downcase)
68
+ else value
69
+ end
70
+ rescue ArgumentError
71
+ raise Runtime::TypeMismatch, "#{value.inspect} is not #{type} — the chapter declares this field as #{type}"
72
+ end
73
+
74
+ # A LIST GROWS RATHER THAN OVERWRITES, and getting this wrong is silent.
75
+ #
76
+ # `tags.value=framework tags.value=model-checker` used to reach `bury`
77
+ # twice and store the second one alone — no refusal, no warning, one tag
78
+ # simply gone. That is the failure the interview named first: not the
79
+ # loud kind, the kind where a value is forgotten and the caller has no
80
+ # way to notice.
81
+ #
82
+ # A LIST OF ONE IS STILL A LIST. `tags.value=flaky` produces
83
+ # `[{ value: "flaky" }]`, not `{ value: "flaky" }`, because the chapter
84
+ # declared a collection and a caller who sent one element did not
85
+ # thereby declare a different shape. The old behaviour handed a bare
86
+ # object to a `list_of` attribute, and everything downstream that walks
87
+ # it — a query's `contains`, a projection, the Postgres adapter's own
88
+ # array handling — is entitled to assume it can iterate.
89
+ #
90
+ # MULTI-FIELD ELEMENTS ARE NOT SUPPORTED HERE, deliberately. A flat
91
+ # command line has no way to say which `a.x=` goes with which `a.y=`,
92
+ # and inventing an index syntax would be a language nobody asked for.
93
+ # Every list in this corpus is a list of single-field value objects; a
94
+ # richer one is a job for `JsonDoor`, which has real nesting.
95
+ def append(hash, path, value)
96
+ *branches, leaf = path.map(&:to_sym)
97
+ holder = branches[0..-2].reduce(hash) { |node, key| node[key] ||= {} }
98
+ list = holder[branches.last] ||= []
99
+
100
+ list << { leaf => value }
101
+ hash
102
+ end
103
+
104
+ def bury(hash, path, value)
105
+ *branches, leaf = path.map(&:to_sym)
106
+ target = branches.reduce(hash) { |node, key| node[key] ||= {} }
107
+ target[leaf] = value
108
+ hash
109
+ end
110
+
111
+ def unknown(path, known)
112
+ "no argument #{path.inspect} — this verb takes #{known.sort.join(', ')}"
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,190 @@
1
+ require "json"
2
+ require_relative "cli_door"
3
+ require_relative "command_request"
4
+ require_relative "json_door"
5
+ require_relative "../projector"
6
+ require_relative "../ports/clock"
7
+
8
+ module Hecks
9
+ module Facade
10
+ # THE RUNNER BEHIND A PROJECTED CLI.
11
+ #
12
+ # `Projector::CliProjector` answers what a domain's command line LOOKS
13
+ # like; this is the twenty lines that parse against it and dispatch. It
14
+ # lives in `lib/` rather than in a `bin/` because more than one front door
15
+ # wants it — `bin/run` for whichever domain you are standing in, `bin/qc`
16
+ # pinned to the QA ledger — and a second copy of the parse-and-dispatch
17
+ # would be the exact duplication the projection exists to avoid.
18
+ #
19
+ # NO IO. It answers `[text, status]` and never prints or exits, so a spec
20
+ # can call it without capturing streams or trapping SystemExit. The `bin/`
21
+ # scripts do the printing, the same division `Router` and `JsonDoor`
22
+ # already keep against HTTP.
23
+ module CliRunner
24
+ module_function
25
+
26
+ # `[text, status]` — status 0 answered, 1 refused or misused.
27
+ def call(runtime:, argv:, program: "bin/run")
28
+ bluebook = runtime.registry.bluebooks.values.first
29
+ cli = Projector.call(:cli, bluebook: bluebook, options: { program: program })
30
+
31
+ name = argv.first
32
+ return [cli[:usage], 0] if name.nil? || %w[--help -h help].include?(name)
33
+
34
+ # `ask` PUTS A QUESTION IN ITS OWN NAMESPACE — a chapter may declare a
35
+ # command and a query of one name, and banking does.
36
+ asking = name == "ask"
37
+ argv = argv[1..] if asking
38
+ name = argv.first
39
+ return [cli[:usage], 1] if name.nil?
40
+
41
+ # RESOLVED THROUGH THE ALIAS MAP, so `pizzas create_pizza` and
42
+ # `pizzas order.create_pizza` reach the same verb — the aggregate is
43
+ # worth typing only when two of them declare the same word.
44
+ pool = asking ? cli[:questions] : cli[:verbs]
45
+ key = cli[:names][asking ? :question : :command][name]
46
+ spec = pool[key]
47
+ return [unknown(cli, name, asking, program), 1] unless spec
48
+
49
+ rest = argv[1..]
50
+ if rest.include?("--help")
51
+ help = Projector.call(:cli, bluebook: bluebook,
52
+ options: { program: program, verb: name, ask: asking })[:usage]
53
+ return [help, 0]
54
+ end
55
+
56
+ dispatch(runtime, spec, name, rest, program, asking)
57
+ end
58
+
59
+ def dispatch(runtime, spec, name, rest, program, asking)
60
+ args = stamp_time(runtime, spec, CliDoor.arguments(spec, rest))
61
+
62
+ if spec[:kind] == :query
63
+ rows = runtime.query(spec[:verb], **args)
64
+ return [JSON.pretty_generate(rows.map { |row| JsonDoor.materialize(row) }), 0]
65
+ end
66
+
67
+ # THE ANSWER IS SCOPED TO WHAT WAS ASKED. `bin/run`'s step-list form
68
+ # reports the whole store because a corpus run is judged on all of it;
69
+ # somebody who issued one verb wants that verb's outcome, and against a
70
+ # Postgres-backed domain the full dump is every record there has been.
71
+ request = CommandRequest.normalize(args, receiver: spec[:receiver],
72
+ legacy_receiver: spec[:legacy_receiver])
73
+ handle = runtime.dispatch(spec[:verb], **request)
74
+ return [JSON.pretty_generate(answered(handle)), 0] if handle.state.nil?
75
+
76
+ [JSON.pretty_generate(id: handle.id,
77
+ state: JsonDoor.materialize(handle.state),
78
+ events: handle.events.map(&:name)), 0]
79
+ rescue Runtime::NotFound, Runtime::TypeMismatch => e
80
+ # A BAD ARGUMENT AND A MISSING RECORD BOTH LAND HERE, and both want the
81
+ # same next step: read what the verb actually takes.
82
+ ["#{e.message}\n\n #{program} #{asking ? 'ask ' : ''}#{name} --help", 1]
83
+ rescue *Runtime::DOMAIN_REFUSALS => e
84
+ # THE REFUSAL IS THE PRODUCT — the chapter's own sentence, verbatim.
85
+ [e.message, 1]
86
+ end
87
+
88
+ # THE CLOCK, FILLED IN AT THE DOOR.
89
+ #
90
+ # A staleness rule needs the time, and the sublanguage cannot ask for it
91
+ # — a `given` that read the clock would judge the same record differently
92
+ # on two runs, and every replay, audit and fuzz oracle here assumes it
93
+ # does not. So `now` stays an ARGUMENT the predicate merely reads, and
94
+ # the question becomes who types it. Before this, the caller did:
95
+ #
96
+ # qa/quality_control target.claim id=QC held_by.value=me \
97
+ # now.value=$(date +%s) window.value=900
98
+ #
99
+ # which is a shell incantation in front of every claim, and one an agent
100
+ # gets wrong by pasting a stale number.
101
+ #
102
+ # AT THE DOOR, NOT IN THE RUNTIME, and the distinction is load-bearing.
103
+ # `Ports::IdentityGeneration` reasons the same question through for a
104
+ # minted uuid and lands on "the value is baked into the caller's args at
105
+ # the first live dispatch". A clock consulted INSIDE the interpreter
106
+ # would not have that property — a recorded corpus step replayed
107
+ # tomorrow would quietly get tomorrow's time, and the fuzzer's oracle and
108
+ # the adapter-agreement gate both compare runs of exactly that shape. Here
109
+ # it fills only what a person or an agent is typing, and `runtime.dispatch`
110
+ # is left alone.
111
+ #
112
+ # AN EXPLICIT VALUE ALWAYS WINS, so a spec or a caller reproducing a
113
+ # moment says so and is believed. This only supplies what was omitted.
114
+ #
115
+ # BY NAME, WHICH IS THE ONE UNCOMFORTABLE PART. `now` is a plausible
116
+ # domain word and nothing declares that it means the clock. It is
117
+ # tolerable because this is a convenience layer rather than semantics —
118
+ # the verb's own help says the argument exists, dispatch is unchanged,
119
+ # and passing it explicitly is always available. The honest version is a
120
+ # declaration in the chapter (`attribute :now, Instant, from: :clock`),
121
+ # which is a language change: DSL, IR, the self-hosted grammar and its
122
+ # goldens. Worth doing; not worth smuggling in here.
123
+ def stamp_time(runtime, spec, args)
124
+ return args unless spec[:arguments].any? { |argument| argument[:path].start_with?("now.") }
125
+ return args if args.key?(:now)
126
+
127
+ args.merge(now: { value: Ports::Clock.now(runtime.registry) })
128
+ end
129
+
130
+ # A PORT OPERATION HAS NO STATE, AND ITS PAYLOAD IS THE ENTIRE POINT.
131
+ #
132
+ # A command answers with the record it changed, so naming the events is
133
+ # enough — the interesting part is in `state`. A port operation changes
134
+ # no record: it asked something outside and came back with what was
135
+ # said, and that lives ONLY in the event payload. Reporting names alone
136
+ # would print `SpecsCompleted` and drop the spec output on the floor.
137
+ #
138
+ # This is what makes a projected CLI usable as somebody's only door. An
139
+ # agent that may not shell out cannot run `rspec` and read the terminal;
140
+ # it asks the port and reads the answer, and if the answer is a bare
141
+ # event name then the door leads nowhere and it needs a shell after all.
142
+ #
143
+ # BOTH ENDINGS COME BACK THE SAME WAY, and the status stays 0 for both.
144
+ # A refusal here is not a misuse — `IssueStillOpen` and `SuiteFailed`
145
+ # are answers the caller asked for, correctly delivered. Exit 1 is for
146
+ # "you typed something wrong", and conflating the two would have a
147
+ # scripted agent treat a healthy no as a broken call.
148
+ def answered(handle)
149
+ # THE ID COMES OFF THE EVENT, because a port operation hydrates no
150
+ # instance and the handle's own `id` is nil by design. The event knows
151
+ # which record was asked about — it was stamped with it — and printing
152
+ # `null` beside a payload that plainly says `SW-TOOL` would read as a
153
+ # bug in something.
154
+ { id: handle.id || handle.events.first&.id,
155
+ events: handle.events.map do |event|
156
+ { name: event.name, payload: JsonDoor.materialize(event.payload) }
157
+ end }
158
+ end
159
+
160
+ # A NEAR MISS IS WORTH MORE THAN A LIST. Somebody who typed
161
+ # `bug.discovr` wants one line, not eighty-seven of them.
162
+ #
163
+ # RANKED BY SHARED PREFIX, not by substring. Substring was the first
164
+ # attempt and it finds nothing for the commonest typo of all — a dropped
165
+ # letter, `order.create_piza`, which is a substring of nothing. Prefix
166
+ # length survives an error anywhere after it, which is where errors are.
167
+ def unknown(cli, name, asking, program)
168
+ # BOTH SPELLINGS ARE CANDIDATES. A caller who typed the qualified
169
+ # form with a typo — `order.create_piza` — shares no prefix with the
170
+ # short name `create_pizza`, so pooling only one of them suggests
171
+ # nothing for half the mistakes anybody makes.
172
+ pool = cli[:names][asking ? :question : :command].keys
173
+ near = pool.map { |candidate| [shared_prefix(candidate, name), candidate] }
174
+ .select { |shared, _| shared >= [name.length / 2, 3].max }
175
+ .sort_by { |shared, candidate| [-shared, candidate] }
176
+ .map(&:last)
177
+
178
+ lines = ["no such #{asking ? 'question' : 'verb'}: #{name}"]
179
+ lines += ["", "did you mean:", *near.first(5).map { |candidate| " #{candidate}" }] unless near.empty?
180
+ lines += ["", " #{program}#{asking ? ' ask' : ''} for the full list"]
181
+ lines.join("\n")
182
+ end
183
+
184
+ def shared_prefix(one, other)
185
+ length = [one.length, other.length].min
186
+ (0...length).find { |index| one[index] != other[index] } || length
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,105 @@
1
+ require_relative "../runtime/errors"
2
+
3
+ module Hecks
4
+ module Facade
5
+ # Turns an external command request into the dispatcher's receiver/payload
6
+ # envelope. Human-facing doors may accept their old flat id spelling at
7
+ # the edge, but every call leaving this boundary has one shape:
8
+ #
9
+ # aggregate command: { to: "record-id", with: { declared: "facts" } }
10
+ # entity command: { to: { aggregate: "...", entity: "..." },
11
+ # with: { declared: "facts" } }
12
+ #
13
+ # The command interpreter remains the authority on which facts are
14
+ # declared. This helper only prevents routing fields from leaking into the
15
+ # fact payload and gives every external door the same wire contract.
16
+ module CommandRequest
17
+ module_function
18
+
19
+ def normalize(input, receiver:, legacy_receiver: nil)
20
+ request = symbolize(input)
21
+ unless request.is_a?(Hash)
22
+ raise Runtime::TypeMismatch, "a command request must be a hash"
23
+ end
24
+
25
+ route, facts = split(request, receiver: receiver, legacy_receiver: legacy_receiver)
26
+ validate_route!(route, receiver)
27
+
28
+ envelope = { with: facts }
29
+ envelope[:to] = route if receiver
30
+ envelope
31
+ end
32
+
33
+ def split(request, receiver:, legacy_receiver:)
34
+ if request.key?(:with)
35
+ loose = request.keys - [:to, :with]
36
+ unless loose.empty?
37
+ raise Runtime::TypeMismatch,
38
+ "an explicit command envelope takes routing in to: and facts in with:, not loose #{loose.sort.join(', ')}"
39
+ end
40
+
41
+ facts = request[:with]
42
+ raise Runtime::TypeMismatch, "with: must be a hash of command facts" unless facts.is_a?(Hash)
43
+
44
+ return [request[:to], facts]
45
+ end
46
+
47
+ flat = request.dup
48
+ route = flat.delete(:to)
49
+ route = take_legacy_route(flat, receiver, legacy_receiver) if route.nil?
50
+ [route, flat]
51
+ end
52
+ private_class_method :split
53
+
54
+ def take_legacy_route(flat, receiver, legacy_receiver)
55
+ return unless legacy_receiver
56
+
57
+ if receiver == :aggregate
58
+ return flat.delete(legacy_receiver.to_sym)
59
+ end
60
+
61
+ return unless receiver == :entity && legacy_receiver.is_a?(Hash)
62
+
63
+ aggregate_key = legacy_receiver.fetch(:aggregate).to_sym
64
+ entity_key = legacy_receiver.fetch(:entity).to_sym
65
+ return unless flat.key?(aggregate_key) || flat.key?(entity_key)
66
+
67
+ { aggregate: flat.delete(aggregate_key), entity: flat.delete(entity_key) }
68
+ end
69
+ private_class_method :take_legacy_route
70
+
71
+ def validate_route!(route, receiver)
72
+ case receiver
73
+ when nil
74
+ raise Runtime::TypeMismatch, "this command does not take a receiver in to:" unless route.nil?
75
+ when :aggregate
76
+ if route.nil? || route.to_s.empty? || route.is_a?(Hash)
77
+ raise Runtime::TypeMismatch, "to: must name the receiving aggregate identity"
78
+ end
79
+ when :entity
80
+ unless route.is_a?(Hash)
81
+ raise Runtime::TypeMismatch, "to: for an entity command must contain aggregate: and entity:"
82
+ end
83
+
84
+ extra = route.keys - [:aggregate, :entity]
85
+ missing = [:aggregate, :entity].select { |key| route[key].nil? || route[key].to_s.empty? }
86
+ unless extra.empty? && missing.empty?
87
+ raise Runtime::TypeMismatch, "to: for an entity command must contain only aggregate: and entity:"
88
+ end
89
+ else
90
+ raise ArgumentError, "unknown receiver kind #{receiver.inspect}"
91
+ end
92
+ end
93
+ private_class_method :validate_route!
94
+
95
+ def symbolize(value)
96
+ case value
97
+ when Hash then value.to_h { |key, nested| [key.to_sym, symbolize(nested)] }
98
+ when Array then value.map { |nested| symbolize(nested) }
99
+ else value
100
+ end
101
+ end
102
+ private_class_method :symbolize
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,173 @@
1
+ require_relative "../naming"
2
+
3
+ module Hecks
4
+ module Facade
5
+ # ONE record in hand — the object `Pizza.create_pizza(...)` and
6
+ # `Pizza.find(id)` give back.
7
+ #
8
+ # ONE SHARED CLASS, not one minted per aggregate. The old door subclassed
9
+ # `Hecks::Aggregate` per head and defined a reader per field ; this
10
+ # wraps the same `Runtime::Instance` state hash and answers readers and
11
+ # verbs through `method_missing`, closing over the dispatcher and the
12
+ # aggregate's IR — so a boot mints no classes at all, and two boots in
13
+ # one process each hand out handles bound to their own dispatcher.
14
+ #
15
+ # A non-creating verb is a method returning self, so commands chain :
16
+ #
17
+ # Pizza.create_pizza(...).add_topping(...).purchase(...)
18
+ class Handle
19
+ attr_reader :id
20
+
21
+ def initialize(dispatcher:, domain:, ir:, instance:)
22
+ @dispatcher = dispatcher
23
+ @domain = domain
24
+ @ir = ir
25
+ @id = instance.id
26
+ @state = instance.state
27
+ define_reference_accessors
28
+ define_verb_methods
29
+ end
30
+
31
+ def [](key) = @state[key.to_sym]
32
+
33
+ # `id: @id` LAST, not first — an aggregate is free to declare its own
34
+ # attribute literally named `id` (BurningManPrep's `Item`, `attribute
35
+ # :id, ItemId`, is real corpus now: `identified_by :id` reads
36
+ # THAT attribute for identity). When it does, `@state[:id]` holds the
37
+ # full wrapped value object, not the bare identity string — merging
38
+ # `@state` on top of `{ id: @id }` let that wrapped VO silently
39
+ # clobber the correct bare `@id`, so every caller of `to_h` (the JSON
40
+ # door's own `/api/:coll` listing, in particular) got an object where
41
+ # a plain identity string belonged. `@id` merged LAST always wins,
42
+ # so `to_h[:id]` is always the true bare identity, regardless of
43
+ # whether the aggregate also happens to declare a same-named field.
44
+ def to_h = @state.merge(id: @id)
45
+
46
+ def fqn = "#{@domain}::#{@ir.hecks_name}"
47
+
48
+ def events
49
+ @dispatcher.events.select { |event| event.aggregate == fqn && event.id == @id }
50
+ end
51
+
52
+ def reload
53
+ stored = repository.find(@id)
54
+ @state = stored.state if stored
55
+ self
56
+ end
57
+
58
+ # Equality is (WHICH AGGREGATE, WHICH ID) — two handles to the same record
59
+ # are the same record, and a Pizza never equals an Account that happens to
60
+ # share an id. The old door said this with `other.is_a?(self.class)`,
61
+ # leaning on one class per aggregate ; the fqn says it in data.
62
+ def ==(other) = other.is_a?(Handle) && other.fqn == fqn && other.id == @id
63
+ alias eql? ==
64
+ def hash = [Handle, fqn, @id].hash
65
+
66
+ def inspect
67
+ fields = @state.map { |key, value| "#{key}=#{value.inspect}" }.join(" ")
68
+ "#<#{@ir.hecks_name} #{@id} #{fields}>"
69
+ end
70
+ alias to_s inspect
71
+
72
+ # A declared field not yet written arrives here too (nil, the way a
73
+ # defined reader answered). Verbs are NOT handled here — see
74
+ # `define_verb_methods` for why.
75
+ def method_missing(name, *args, **kwargs, &block)
76
+ return @state[name] if @state.key?(name) || reader?(name)
77
+
78
+ super
79
+ end
80
+
81
+ def respond_to_missing?(name, include_private = false)
82
+ @state.key?(name) || reader?(name) || super
83
+ end
84
+
85
+ private
86
+
87
+ def repository = @dispatcher.registry.repository(@domain, @ir)
88
+
89
+ def reader?(name)
90
+ !@ir.attribute(name).nil? || @ir.lifecycle&.field&.to_sym == name
91
+ end
92
+
93
+ # NON-CREATING VERBS ARE DEFINED, NOT DISPATCHED THROUGH method_missing.
94
+ #
95
+ # method_missing only runs once Ruby finds no REAL method already
96
+ # answering the name — and every object already answers `freeze` and
97
+ # `send` (Kernel/Object), among others. A verb whose snake-cased name
98
+ # collided with one of those — `Account::Freeze` -> `freeze`,
99
+ # `ExternalTransfer::Send` -> `send` in the banking corpus, both real —
100
+ # used to silently run the Kernel method instead of dispatching: no
101
+ # error, no refusal, the call just did the wrong thing. Defining a
102
+ # real singleton method per verb closed that; the `!` suffix (every
103
+ # command, door and Handle alike) closes it a second, permanent way —
104
+ # `freeze!`/`send!` name nothing Kernel/Object already answers to,
105
+ # so this exact class of collision cannot recur no matter what a
106
+ # future domain names a command.
107
+ def define_verb_methods
108
+ @ir.commands.reject(&:creates?).each do |command|
109
+ define_singleton_method("#{Naming.snake(command.hecks_name)}!") do |**args|
110
+ run(command, **args)
111
+ end
112
+ end
113
+ end
114
+
115
+ # ONE HEAD ADDRESSES THE SAME WAY AS SEVERAL. `@ir.identified_by` is only
116
+ # the single-head shorthand — nil the moment an identity is composite
117
+ # (`SafeDepositBox`'s `branch_code`/`box_number`) — so building the
118
+ # identity payload from `identity_heads` instead reads every head, one
119
+ # or many alike, straight out of state that already carries them.
120
+ def run(command, **args)
121
+ @state = @dispatcher.dispatch("#{fqn}.#{command.hecks_name}", to: @id, with: args).instance.state
122
+ self
123
+ end
124
+
125
+ # THE OTHER HALF OF A CROSS-REFERENCE. `transfer.source` already reads
126
+ # the raw value — a plain reader, same as any other attribute, still
127
+ # needed by a `given`. This is the hydrated hop docs/rails-integration.md
128
+ # designed and marked "nothing built": `transfer.source_account`
129
+ # resolves it to the actual Account record, on demand — nothing loads
130
+ # until called, and this hop never triggers the next one. Plain
131
+ # chaining composes for free from here : `payment.disputed_by_customer.name`
132
+ # is two ordinary calls, each individually lazy, which is exactly why
133
+ # this is a named accessor per reference rather than a `through:`
134
+ # option — that shape was considered and rejected in the same design
135
+ # note for hiding how many lookups actually happened behind one call.
136
+ #
137
+ # Defined BEFORE verb methods, not after — on the vanishing chance a
138
+ # reference's own accessor name collided with a command's, the verb
139
+ # should win; `initialize` calls this first so `define_verb_methods`
140
+ # defines second and last.
141
+ # NO DERIVATION LEFT (ADR 0025, "References"): `reference_to`
142
+ # itself mints the bare attribute name now — `:account`, never
143
+ # `:account_id` — so the accessor is spelled exactly like the
144
+ # attribute it reads, with no `_id`-strip or `as:`-suffix rule to
145
+ # apply first. `piece.account` (a METHOD, defined here) and
146
+ # `piece[:account]` (`Handle#[]`, bracket access reading the raw
147
+ # id straight off `@instance`) never collide despite sharing a
148
+ # name — Ruby dispatches the two completely differently — which is
149
+ # what makes the OLD "studio_studio" double-suffix workaround
150
+ # (this method used to force a DIFFERENT name specifically to dodge
151
+ # that non-collision) unnecessary rather than merely simplified.
152
+ def define_reference_accessors
153
+ @ir.attributes.select(&:reference?).each do |attribute|
154
+ target = attribute.type.resolve
155
+ next unless target # cross-domain, or otherwise unresolvable — no accessor rather than a guess
156
+
157
+ domain = @domain
158
+ field = attribute.name
159
+ list = attribute.list?
160
+ target_fqn = "#{domain}::#{target.hecks_name}"
161
+
162
+ define_singleton_method(field) do
163
+ value = self[field]
164
+ door = Object.const_get(target_fqn)
165
+ next Array(value).map { |identity| door.find(identity) } if list
166
+
167
+ value && door.find(value)
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end