hecks 0.2.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (419) hide show
  1. checksums.yaml +5 -5
  2. data/lib/hecks/adapters/driven/claude_code.adapter +3 -0
  3. data/lib/hecks/adapters/driven/claude_code.rb +127 -0
  4. data/lib/hecks/adapters/driven/d1.adapter +12 -0
  5. data/lib/hecks/adapters/driven/d1.rb +427 -0
  6. data/lib/hecks/adapters/driven/folder.adapter +3 -0
  7. data/lib/hecks/adapters/driven/folder.rb +199 -0
  8. data/lib/hecks/adapters/driven/google_authentication.adapter +3 -0
  9. data/lib/hecks/adapters/driven/google_authentication.rb +100 -0
  10. data/lib/hecks/adapters/driven/governance_authorization.adapter +3 -0
  11. data/lib/hecks/adapters/driven/governance_authorization.rb +101 -0
  12. data/lib/hecks/adapters/driven/heki/journal.rb +56 -0
  13. data/lib/hecks/adapters/driven/heki/saga_store.rb +99 -0
  14. data/lib/hecks/adapters/driven/heki/snapshot.rb +65 -0
  15. data/lib/hecks/adapters/driven/heki.adapter +4 -0
  16. data/lib/hecks/adapters/driven/heki.rb +179 -0
  17. data/lib/hecks/adapters/driven/identity_registry.adapter +3 -0
  18. data/lib/hecks/adapters/driven/identity_registry.rb +26 -0
  19. data/lib/hecks/adapters/driven/in_memory_ordering.rb +51 -0
  20. data/lib/hecks/adapters/driven/lambda/client.rb +63 -0
  21. data/lib/hecks/adapters/driven/lambda.adapter +4 -0
  22. data/lib/hecks/adapters/driven/lambda.rb +145 -0
  23. data/lib/hecks/adapters/driven/memory.adapter +3 -0
  24. data/lib/hecks/adapters/driven/memory.rb +103 -0
  25. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +3 -0
  26. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +28 -0
  27. data/lib/hecks/adapters/driven/postgres/codec.rb +88 -0
  28. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +207 -0
  29. data/lib/hecks/adapters/driven/postgres.adapter +5 -0
  30. data/lib/hecks/adapters/driven/postgres.rb +438 -0
  31. data/lib/hecks/adapters/driven/postgres_era.adapter +17 -0
  32. data/lib/hecks/adapters/driven/prism.adapter +3 -0
  33. data/lib/hecks/adapters/driven/prism.rb +80 -0
  34. data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
  35. data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
  36. data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
  37. data/lib/hecks/adapters/driven/sqlite/codec.rb +80 -0
  38. data/lib/hecks/adapters/driven/sqlite/projection.rb +172 -0
  39. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +192 -0
  40. data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
  41. data/lib/hecks/adapters/driven/sqlite.rb +306 -0
  42. data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
  43. data/lib/hecks/adapters/driven/system_clock.rb +14 -0
  44. data/lib/hecks/adapters/driven.rb +54 -0
  45. data/lib/hecks/adapters.rb +6 -0
  46. data/lib/hecks/behaviors/dsl.rb +125 -0
  47. data/lib/hecks/behaviors/expectations.rb +340 -0
  48. data/lib/hecks/behaviors/ir.rb +31 -0
  49. data/lib/hecks/behaviors/rspec.rb +42 -0
  50. data/lib/hecks/behaviors/runner.rb +96 -0
  51. data/lib/hecks/behaviors.rb +25 -0
  52. data/lib/hecks/bluebook/aggregate.rb +108 -0
  53. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
  54. data/lib/hecks/bluebook/assembly/build.rb +48 -0
  55. data/lib/hecks/bluebook/assembly/contract.rb +112 -0
  56. data/lib/hecks/bluebook/assembly/contracts.rb +438 -0
  57. data/lib/hecks/bluebook/assembly/marks.rb +228 -0
  58. data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
  59. data/lib/hecks/bluebook/assembly.rb +91 -0
  60. data/lib/hecks/bluebook/attribute.rb +96 -0
  61. data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
  62. data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
  63. data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
  64. data/lib/hecks/bluebook/behaviour/command.rb +116 -0
  65. data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
  66. data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
  67. data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
  68. data/lib/hecks/bluebook/behaviour/lifecycle.rb +68 -0
  69. data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
  70. data/lib/hecks/bluebook/behaviour/process_manager.rb +51 -0
  71. data/lib/hecks/bluebook/behaviour/query.rb +10 -0
  72. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
  73. data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
  74. data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
  75. data/lib/hecks/bluebook/chapter.rb +78 -0
  76. data/lib/hecks/bluebook/command.rb +124 -0
  77. data/lib/hecks/bluebook/domain_port.rb +102 -0
  78. data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
  79. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +1018 -0
  80. data/lib/hecks/bluebook/dsl/attribute_collector.rb +348 -0
  81. data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
  82. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1087 -0
  83. data/lib/hecks/bluebook/dsl/command_builder.rb +714 -0
  84. data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
  85. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
  86. data/lib/hecks/bluebook/dsl/entity_builder.rb +430 -0
  87. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
  88. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
  89. data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
  90. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
  91. data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
  92. data/lib/hecks/bluebook/dsl/policy_builder.rb +135 -0
  93. data/lib/hecks/bluebook/dsl/port_builder.rb +39 -0
  94. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
  95. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +306 -0
  96. data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
  97. data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
  98. data/lib/hecks/bluebook/dsl/rule_reference.rb +173 -0
  99. data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
  100. data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
  101. data/lib/hecks/bluebook/dsl/word_gate.rb +228 -0
  102. data/lib/hecks/bluebook/dsl/world_builder.rb +117 -0
  103. data/lib/hecks/bluebook/dsl.rb +45 -0
  104. data/lib/hecks/bluebook/entity.rb +103 -0
  105. data/lib/hecks/bluebook/expression/canonical_form.rb +123 -0
  106. data/lib/hecks/bluebook/expression/evaluator.rb +304 -0
  107. data/lib/hecks/bluebook/expression/projection.json +218 -0
  108. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
  109. data/lib/hecks/bluebook/expression/resolver.rb +781 -0
  110. data/lib/hecks/bluebook/expression.rb +13 -0
  111. data/lib/hecks/bluebook/hexagon.rb +60 -0
  112. data/lib/hecks/bluebook/lifecycle.rb +42 -0
  113. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
  114. data/lib/hecks/bluebook/meta_validator/judge.rb +621 -0
  115. data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
  116. data/lib/hecks/bluebook/meta_validator/port_judge.rb +51 -0
  117. data/lib/hecks/bluebook/meta_validator/readings.rb +360 -0
  118. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +351 -0
  119. data/lib/hecks/bluebook/meta_validator/shapes.rb +266 -0
  120. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
  121. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
  122. data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
  123. data/lib/hecks/bluebook/meta_validator.rb +542 -0
  124. data/lib/hecks/bluebook/model_check.rb +445 -0
  125. data/lib/hecks/bluebook/pattern_subset.rb +184 -0
  126. data/lib/hecks/bluebook/policy.rb +41 -0
  127. data/lib/hecks/bluebook/process_manager.rb +128 -0
  128. data/lib/hecks/bluebook/project_discovery.rb +30 -0
  129. data/lib/hecks/bluebook/project_loader.rb +40 -0
  130. data/lib/hecks/bluebook/project_register.rb +82 -0
  131. data/lib/hecks/bluebook/query.rb +61 -0
  132. data/lib/hecks/bluebook/read_model.rb +109 -0
  133. data/lib/hecks/bluebook/reference.rb +74 -0
  134. data/lib/hecks/bluebook/smoke_test.rb +166 -0
  135. data/lib/hecks/bluebook/synthesizer.rb +95 -0
  136. data/lib/hecks/bluebook/translation.rb +92 -0
  137. data/lib/hecks/bluebook/value_object.rb +66 -0
  138. data/lib/hecks/bluebook.rb +74 -0
  139. data/lib/hecks/codemod.rb +342 -0
  140. data/lib/hecks/construct.rb +71 -0
  141. data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
  142. data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
  143. data/lib/hecks/deploy/oidc.json +18 -0
  144. data/lib/hecks/doc/reference.rb +410 -0
  145. data/lib/hecks/embryonaut_bluebook.rb +75 -0
  146. data/lib/hecks/facade/cli_door.rb +119 -0
  147. data/lib/hecks/facade/cli_runner.rb +190 -0
  148. data/lib/hecks/facade/command_request.rb +105 -0
  149. data/lib/hecks/facade/handle.rb +173 -0
  150. data/lib/hecks/facade/json_door.rb +166 -0
  151. data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
  152. data/lib/hecks/facade/surface/chapter.rb +107 -0
  153. data/lib/hecks/facade/surface.rb +48 -0
  154. data/lib/hecks/facade.rb +44 -0
  155. data/lib/hecks/forms/app.rb +341 -0
  156. data/lib/hecks/forms/command_form_renderer.rb +113 -0
  157. data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
  158. data/lib/hecks/forms/field_renderer.rb +177 -0
  159. data/lib/hecks/forms/field_shape.rb +232 -0
  160. data/lib/hecks/forms/html.rb +84 -0
  161. data/lib/hecks/forms/index_renderer.rb +35 -0
  162. data/lib/hecks/forms/page.rb +157 -0
  163. data/lib/hecks/forms/params.rb +161 -0
  164. data/lib/hecks/forms/port_argument.rb +46 -0
  165. data/lib/hecks/forms/query_form_renderer.rb +114 -0
  166. data/lib/hecks/forms/record_renderer.rb +119 -0
  167. data/lib/hecks/forms/record_table.rb +68 -0
  168. data/lib/hecks/forms/reference_options.rb +30 -0
  169. data/lib/hecks/forms/value_object_shape.rb +46 -0
  170. data/lib/hecks/forms.rb +54 -0
  171. data/lib/hecks/fqn.rb +94 -0
  172. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
  173. data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
  174. data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
  175. data/lib/hecks/framework/bluebook/governance.bluebook +145 -0
  176. data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
  177. data/lib/hecks/framework/oidc.json +39 -0
  178. data/lib/hecks/framework.rb +90 -0
  179. data/lib/hecks/freezer.rb +67 -0
  180. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +527 -0
  181. data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
  182. data/lib/hecks/fuzzing/isolated_boot.rb +286 -0
  183. data/lib/hecks/fuzzing/properties.rb +1192 -0
  184. data/lib/hecks/fuzzing/replay.rb +668 -0
  185. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
  186. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +80 -0
  187. data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
  188. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +250 -0
  189. data/lib/hecks/fuzzing/sequence_generator.rb +131 -0
  190. data/lib/hecks/fuzzing/value_generator.rb +200 -0
  191. data/lib/hecks/fuzzing.rb +12 -0
  192. data/lib/hecks/grammar/evolve.rb +324 -0
  193. data/lib/hecks/grammar/expression.bluebook +420 -0
  194. data/lib/hecks/grammar/expression_operators.json +1648 -0
  195. data/lib/hecks/grammar/grammar.hecksagon +20 -0
  196. data/lib/hecks/grammar/oidc.json +69 -0
  197. data/lib/hecks/grammar/translation.bluebook +203 -0
  198. data/lib/hecks/grammar.rb +178 -0
  199. data/lib/hecks/ir.rb +126 -0
  200. data/lib/hecks/language/adapter.bluebook +116 -0
  201. data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
  202. data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
  203. data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
  204. data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
  205. data/lib/hecks/language/bluebook/command.bluebook +471 -0
  206. data/lib/hecks/language/bluebook/entity.bluebook +392 -0
  207. data/lib/hecks/language/bluebook/policy.bluebook +189 -0
  208. data/lib/hecks/language/bluebook/process_manager.bluebook +381 -0
  209. data/lib/hecks/language/bluebook/projection.bluebook +267 -0
  210. data/lib/hecks/language/bluebook/query.bluebook +245 -0
  211. data/lib/hecks/language/bluebook/shape.bluebook +292 -0
  212. data/lib/hecks/language/bluebook/syntax.bluebook +448 -0
  213. data/lib/hecks/language/bluebook/vocabulary.bluebook +379 -0
  214. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
  215. data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
  216. data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
  217. data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
  218. data/lib/hecks/language/oidc.json +333 -0
  219. data/lib/hecks/language/port.bluebook +120 -0
  220. data/lib/hecks/language/translation/translation.bluebook +110 -0
  221. data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
  222. data/lib/hecks/language/world/wiring.bluebook +62 -0
  223. data/lib/hecks/language/world/world.bluebook +84 -0
  224. data/lib/hecks/literal.rb +125 -0
  225. data/lib/hecks/naming.rb +174 -0
  226. data/lib/hecks/ports/access_control.port +9 -0
  227. data/lib/hecks/ports/access_control.rb +62 -0
  228. data/lib/hecks/ports/agent/answers.rb +104 -0
  229. data/lib/hecks/ports/agent.port +8 -0
  230. data/lib/hecks/ports/agent.rb +167 -0
  231. data/lib/hecks/ports/authentication.port +6 -0
  232. data/lib/hecks/ports/authentication.rb +50 -0
  233. data/lib/hecks/ports/authorization.port +7 -0
  234. data/lib/hecks/ports/authorization.rb +62 -0
  235. data/lib/hecks/ports/clock.port +5 -0
  236. data/lib/hecks/ports/clock.rb +62 -0
  237. data/lib/hecks/ports/extraction.port +5 -0
  238. data/lib/hecks/ports/extraction.rb +37 -0
  239. data/lib/hecks/ports/identity_assignment.port +5 -0
  240. data/lib/hecks/ports/identity_assignment.rb +45 -0
  241. data/lib/hecks/ports/identity_generation.port +5 -0
  242. data/lib/hecks/ports/identity_generation.rb +49 -0
  243. data/lib/hecks/ports/identity_resolution.port +5 -0
  244. data/lib/hecks/ports/identity_resolution.rb +40 -0
  245. data/lib/hecks/ports/loading.port +4 -0
  246. data/lib/hecks/ports/loading.rb +13 -0
  247. data/lib/hecks/ports/persistence/append_only.rb +138 -0
  248. data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
  249. data/lib/hecks/ports/persistence/execution.rb +23 -0
  250. data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
  251. data/lib/hecks/ports/persistence/plugin.rb +54 -0
  252. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +169 -0
  253. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +128 -0
  254. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +161 -0
  255. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +61 -0
  256. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +304 -0
  257. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +171 -0
  258. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +190 -0
  259. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +478 -0
  260. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +166 -0
  261. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +314 -0
  262. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +168 -0
  263. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +170 -0
  264. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +134 -0
  265. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +137 -0
  266. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +89 -0
  267. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +85 -0
  268. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
  269. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +132 -0
  270. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +74 -0
  271. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +758 -0
  272. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +120 -0
  273. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +31 -0
  274. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +46 -0
  275. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +102 -0
  276. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +46 -0
  277. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +70 -0
  278. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +72 -0
  279. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +120 -0
  280. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +183 -0
  281. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +41 -0
  282. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +28 -0
  283. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +30 -0
  284. data/lib/hecks/ports/persistence/plugins/era/translation.rb +11 -0
  285. data/lib/hecks/ports/persistence/plugins/era.rb +48 -0
  286. data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
  287. data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
  288. data/lib/hecks/ports/persistence.port +4 -0
  289. data/lib/hecks/ports/persistence.rb +39 -0
  290. data/lib/hecks/ports/projection.port +4 -0
  291. data/lib/hecks/ports/projection.rb +96 -0
  292. data/lib/hecks/ports/query/in_memory.rb +60 -0
  293. data/lib/hecks/ports/query/ordering.rb +41 -0
  294. data/lib/hecks/ports/query.rb +36 -0
  295. data/lib/hecks/ports.rb +27 -0
  296. data/lib/hecks/projections/diagrams.rb +642 -0
  297. data/lib/hecks/projections/ir.rb +18 -0
  298. data/lib/hecks/projections/model/deviations.rb +98 -0
  299. data/lib/hecks/projections/model.rb +145 -0
  300. data/lib/hecks/projections/oidc.rb +110 -0
  301. data/lib/hecks/projections/parser_table.rb +159 -0
  302. data/lib/hecks/projections/reference.rb +38 -0
  303. data/lib/hecks/projections/shape.rb +44 -0
  304. data/lib/hecks/projections/statements.rb +110 -0
  305. data/lib/hecks/projections/vocabulary.rb +100 -0
  306. data/lib/hecks/projections.rb +32 -0
  307. data/lib/hecks/projector/cli_projector.rb +446 -0
  308. data/lib/hecks/projector/docs_projector.rb +321 -0
  309. data/lib/hecks/projector/exporter.rb +158 -0
  310. data/lib/hecks/projector/ir_projector.rb +18 -0
  311. data/lib/hecks/projector/narrate_projector.rb +243 -0
  312. data/lib/hecks/projector/target.rb +97 -0
  313. data/lib/hecks/projector.rb +186 -0
  314. data/lib/hecks/query_ir.rb +411 -0
  315. data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
  316. data/lib/hecks/query_specification/common/comparators.rb +25 -0
  317. data/lib/hecks/query_specification/common/comparison.rb +174 -0
  318. data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
  319. data/lib/hecks/query_specification/common/dsl.rb +58 -0
  320. data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
  321. data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
  322. data/lib/hecks/query_specification/common/null_policy.rb +92 -0
  323. data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
  324. data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
  325. data/lib/hecks/query_specification/common/options.rb +32 -0
  326. data/lib/hecks/query_specification/common/order_by.rb +9 -0
  327. data/lib/hecks/query_specification/common/specification.rb +10 -0
  328. data/lib/hecks/query_specification/common/where_clause.rb +9 -0
  329. data/lib/hecks/query_specification/field_path.rb +107 -0
  330. data/lib/hecks/query_specification/hop_path.rb +132 -0
  331. data/lib/hecks/query_specification/read_model/specification.rb +18 -0
  332. data/lib/hecks/query_specification.rb +14 -0
  333. data/lib/hecks/rendering.rb +48 -0
  334. data/lib/hecks/router/namespace_installer.rb +157 -0
  335. data/lib/hecks/router.rb +70 -0
  336. data/lib/hecks/runtime/aggregate_lock.rb +45 -0
  337. data/lib/hecks/runtime/boot_gates.rb +41 -0
  338. data/lib/hecks/runtime/caller.rb +66 -0
  339. data/lib/hecks/runtime/capability_graph.rb +44 -0
  340. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +135 -0
  341. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +260 -0
  342. data/lib/hecks/runtime/command_interpreter.rb +497 -0
  343. data/lib/hecks/runtime/command_rules/admissibility.rb +357 -0
  344. data/lib/hecks/runtime/command_rules/arithmetic.rb +260 -0
  345. data/lib/hecks/runtime/command_rules/authorization.rb +65 -0
  346. data/lib/hecks/runtime/command_rules/emission.rb +34 -0
  347. data/lib/hecks/runtime/command_rules/references.rb +189 -0
  348. data/lib/hecks/runtime/command_rules.rb +28 -0
  349. data/lib/hecks/runtime/dependency_planning.rb +245 -0
  350. data/lib/hecks/runtime/dispatcher.rb +301 -0
  351. data/lib/hecks/runtime/entity_element.rb +253 -0
  352. data/lib/hecks/runtime/entity_interpreter.rb +299 -0
  353. data/lib/hecks/runtime/errors.rb +122 -0
  354. data/lib/hecks/runtime/event.rb +51 -0
  355. data/lib/hecks/runtime/identity.rb +156 -0
  356. data/lib/hecks/runtime/instance.rb +175 -0
  357. data/lib/hecks/runtime/interpreting.rb +90 -0
  358. data/lib/hecks/runtime/loader.rb +184 -0
  359. data/lib/hecks/runtime/policy_interpreter.rb +366 -0
  360. data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
  361. data/lib/hecks/runtime/query_interpreter.rb +286 -0
  362. data/lib/hecks/runtime/reaction_invocation.rb +253 -0
  363. data/lib/hecks/runtime/read_model_interpreter.rb +341 -0
  364. data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
  365. data/lib/hecks/runtime/reference_hop.rb +99 -0
  366. data/lib/hecks/runtime/refusal_wording.rb +126 -0
  367. data/lib/hecks/runtime/registry/saga_persistence.rb +142 -0
  368. data/lib/hecks/runtime/registry/verification.rb +246 -0
  369. data/lib/hecks/runtime/registry.rb +279 -0
  370. data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
  371. data/lib/hecks/runtime/routing.rb +96 -0
  372. data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
  373. data/lib/hecks/runtime/saga_interpreter.rb +503 -0
  374. data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -0
  375. data/lib/hecks/runtime/tenant_check.rb +84 -0
  376. data/lib/hecks/runtime/tenant_scope.rb +55 -0
  377. data/lib/hecks/runtime/value/admission.rb +130 -0
  378. data/lib/hecks/runtime/value/coercion.rb +565 -0
  379. data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
  380. data/lib/hecks/runtime/value.rb +125 -0
  381. data/lib/hecks/runtime.rb +107 -0
  382. data/lib/hecks/storehouse.rb +632 -0
  383. data/lib/hecks/version.rb +16 -0
  384. data/lib/hecks/vocabulary.rb +218 -0
  385. data/lib/hecks.rb +123 -6
  386. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
  387. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
  388. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
  389. metadata +412 -222
  390. data/bin/hecks +0 -7
  391. data/bin/hecks-package +0 -65
  392. data/bin/hecks_console +0 -12
  393. data/bin/hecks_serverless +0 -6
  394. data/lib/cli/build.rb +0 -14
  395. data/lib/cli/command_runner.rb +0 -28
  396. data/lib/cli/console.rb +0 -10
  397. data/lib/cli/generate.rb +0 -37
  398. data/lib/cli/hecks-cli.rb +0 -27
  399. data/lib/cli/test.rb +0 -57
  400. data/lib/console/commands.rb +0 -8
  401. data/lib/console/hecks-console.rb +0 -1
  402. data/lib/packager/README.md +0 -0
  403. data/lib/packager/app_runner.rb +0 -21
  404. data/lib/packager/args.rb +0 -26
  405. data/lib/packager/compatibility/fixnum.rb +0 -6
  406. data/lib/packager/hecks.rb +0 -39
  407. data/lib/packager/query_runner.rb +0 -21
  408. data/lib/packager/resources/Dockerfile +0 -11
  409. data/lib/packager/resources/app_binary +0 -7
  410. data/lib/packager/resources/bundle_config +0 -3
  411. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
  412. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
  413. data/lib/serverless/Domain +0 -32
  414. data/lib/serverless/cli.rb +0 -75
  415. data/lib/serverless/resources/command_name.js +0 -5
  416. data/lib/serverless/resources/environment.js +0 -7
  417. data/lib/serverless/resources/handler.js.tt +0 -28
  418. data/lib/serverless/resources/run_binary.js +0 -22
  419. data/lib/serverless/resources/serverless.yml +0 -20
@@ -0,0 +1,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
@@ -0,0 +1,166 @@
1
+ require "json"
2
+ require_relative "handle"
3
+ require_relative "command_request"
4
+ require_relative "../naming"
5
+ require_relative "../runtime/errors"
6
+ require_relative "../runtime/value"
7
+
8
+ module Hecks
9
+ module Facade
10
+ # THE JSON DOOR — WHERE Facade MEETS BODY-IN/BODY-OUT CALLERS.
11
+ #
12
+ # `Handle`/`Surface` are Ruby sugar over the dispatcher for a Ruby caller
13
+ # holding real objects — a symbol verb name, a `**kwargs` payload, a
14
+ # `Handle` back in hand. A REST-ish JSON API is a caller holding STRINGS
15
+ # instead: a URL segment naming a collection, a URL segment naming a
16
+ # record or a verb, a parsed request body whose every key arrived as a
17
+ # String because that is all JSON ever gives. Every app that wants to put
18
+ # a JSON API in front of a booted domain has to do that translation —
19
+ # name to class, string to symbol, nested `Runtime::Value` back to plain
20
+ # data — and one sibling app had already hand-written it once, bespoke,
21
+ # against its own routes, before this existed. This is that translation
22
+ # pulled out, generic, reading the SAME IR the rest of the facade already
23
+ # reads rather than re-deriving "how do I find an aggregate by name".
24
+ #
25
+ # NO HTTP LIVES HERE. Same discipline `Router` and `Surface` already
26
+ # hold: this module never sees a request object, never picks a status
27
+ # code, never calls `halt`. Every method here takes plain Ruby values in
28
+ # — a raw JSON String is the one exception, see `.parse` below, every
29
+ # other input is already-parsed data — and returns plain Ruby values
30
+ # out, or raises. Turning a raw request body into that input, and
31
+ # turning a raised exception into an HTTP status, both stay the calling
32
+ # app's job, exactly the way they already are for `Router#dispatch`.
33
+ #
34
+ # EVERY "THAT DOESN'T EXIST" CASE RAISES `Runtime::NotFound` — THE SAME
35
+ # CLASS EVERY TIME, NOT A NEW ONE PER CALLER. `Runtime::NotFound` already
36
+ # sits in `Runtime::DOMAIN_REFUSALS` (runtime/errors.rb) — the family a
37
+ # booted app already has, or trivially can have, ONE generic `error`
38
+ # handler for, mapping the whole family to a status code without a
39
+ # special case per refusal. A bespoke `JsonDoor::CollectionNotFound` (or
40
+ # three of those, one per flavor of "not found") would just force every
41
+ # app that adopts this door to widen its rescue clause to match it — the
42
+ # opposite of what a shared refusal family is for. So "no such
43
+ # aggregate", "no creating command", "no such command", and "no record
44
+ # with that id" all raise the one class, distinguished only by message.
45
+ module JsonDoor
46
+ module_function
47
+
48
+ # "Banking", "Customer" -> the `Banking::Customer` class `.find` /
49
+ # `.create_...` / etc already answer for — the same class
50
+ # `Facade::Handle`'s own reference accessors reach with
51
+ # `Object.const_get` (see handle.rb's `define_reference_accessors`).
52
+ #
53
+ # Checked against the CURRENT boot's IR first, not against Ruby's
54
+ # constant table directly — a name that names nothing in this
55
+ # registry should refuse before ever asking Ruby whether some
56
+ # same-named constant happens to exist (possibly a stale one, left
57
+ # over from an earlier boot in this same process — the exact hazard
58
+ # `AggregateDoor#port`'s own comment describes at length). Only once
59
+ # the IR confirms the aggregate is real does this read the constant
60
+ # the current boot's `Surface.install` actually minted for it.
61
+ def aggregate(dispatcher, domain, name)
62
+ ir = dispatcher.registry.bluebook(domain)&.aggregate(name)
63
+ raise Runtime::NotFound, "#{domain} declares no aggregate named #{name.inspect}" unless ir
64
+
65
+ Object.const_get("#{domain}::#{ir.hecks_name}")
66
+ end
67
+
68
+ # The one command a POST to a bare collection URL means — "make one of
69
+ # these". `AggregateDoor` already enforces exactly one creating
70
+ # command per aggregate (`Command#creates?`, true exactly when the
71
+ # command declares no `references`); this just names it the same
72
+ # snake_case-plus-bang a Ruby caller would already ask the door for
73
+ # (`Naming.snake`, the identical call `AggregateDoor` itself makes
74
+ # when it defines that singleton method in the first place — `!`
75
+ # because every command does now, door and Handle alike).
76
+ def creating_command(klass)
77
+ creating = klass.ir.commands.find(&:creates?)
78
+ raise Runtime::NotFound, "#{klass.ir.hecks_name} declares no creating command" unless creating
79
+
80
+ "#{Naming.snake(creating.hecks_name)}!"
81
+ end
82
+
83
+ # A URL segment or a JSON body's "command" field, checked against what
84
+ # a `Handle` can actually dispatch — NOT `klass.commands`, which is
85
+ # `AggregateDoor`'s own door-level list and includes the one creating
86
+ # command too (`aggregate_door.rb`'s `commands` singleton method maps
87
+ # every `ir.commands`, full stop). A `Handle` only ever defines
88
+ # singleton methods for the NON-creating ones
89
+ # (`Handle#define_verb_methods`, `@ir.commands.reject(&:creates?)`) —
90
+ # the creating command lives on the aggregate class itself, dispatched
91
+ # through `.creating_command` above, not through a `Handle` in hand.
92
+ # Accepting a creating-command name here let it past this gate clean,
93
+ # only to blow up as a raw `NoMethodError` the moment a caller tried
94
+ # `handle.public_send(name, **args)`, instead of the 404 this method
95
+ # promises. Filtering `reject(&:creates?)` here, the same filter
96
+ # `Handle` itself applies, is what keeps "accepted here" and
97
+ # "dispatchable there" the same set.
98
+ def validate_command!(klass, name)
99
+ wanted = name.to_s
100
+ dispatchable = klass.ir.commands.reject(&:creates?).map { |command| "#{Naming.snake(command.hecks_name)}!" }
101
+ return wanted if dispatchable.include?(wanted)
102
+
103
+ raise Runtime::NotFound, "#{klass.ir.hecks_name} declares no command named #{wanted.inspect}"
104
+ end
105
+
106
+ # `klass.find` already answers nil-on-miss — the right shape for a
107
+ # Ruby caller that means to check for itself. A JSON caller asking for
108
+ # one record by id off a URL means to HAVE it, or answer 404 — this is
109
+ # that stricter wrapper, raising the same `Runtime::NotFound` the rest
110
+ # of this door raises rather than handing back nil for the caller to
111
+ # remember to check.
112
+ def find!(klass, id)
113
+ klass.find(id) or raise Runtime::NotFound, "no #{klass.ir.hecks_name} found for id #{id.inspect}"
114
+ end
115
+
116
+ # JSON only ever hands back String keys. A command's args, and every
117
+ # nested value-object literal inside them, need symbol keys before
118
+ # `Handle`/`Dispatcher` will accept them at all — this is that
119
+ # recursive conversion, blind to how deep a body nests.
120
+ def deep_symbolize(value)
121
+ case value
122
+ when Hash then value.to_h { |k, v| [k.to_sym, deep_symbolize(v)] }
123
+ when Array then value.map { |item| deep_symbolize(item) }
124
+ else value
125
+ end
126
+ end
127
+
128
+ # The other direction: a `Handle`, or a query row's plain state hash,
129
+ # carrying a `Runtime::Value` at every level a value object sits at —
130
+ # down to plain Ruby a JSON encoder can walk without knowing what a
131
+ # `Runtime::Value` is.
132
+ #
133
+ # `Runtime::Value.materialize` (runtime/value.rb) already does the
134
+ # actual recursion — its `Hash` branch calls itself on every value,
135
+ # its `Value` branch unwraps through `#to_h`, which itself calls
136
+ # `materialize` on every field, so a value object three levels deep
137
+ # unwraps three levels deep for free. This is not a second unwrapper
138
+ # sitting next to it: `materialize` covers `Hash`/`Array`/`Value`
139
+ # already, and a `Handle` is none of those three, so the one thing
140
+ # this adds is `#to_h`'ing a `Handle` first so `materialize`'s own
141
+ # `Hash` case can take it from there.
142
+ def materialize(value)
143
+ value = value.to_h if value.is_a?(Handle)
144
+ Runtime::Value.materialize(value)
145
+ end
146
+
147
+ # Parsed JSON and raw JSON text cross the same receiver/payload boundary
148
+ # as CLI and forms. The result is ready to splat into Dispatcher#dispatch
149
+ # and contains no loose routing fields.
150
+ def command_request(body, receiver:, legacy_receiver: nil)
151
+ input = body.is_a?(String) ? parse(body) : body
152
+ CommandRequest.normalize(input, receiver: receiver, legacy_receiver: legacy_receiver)
153
+ end
154
+
155
+ # The one place a raw JSON string is legitimate input for this door —
156
+ # a POST body, still text at the point a generic, HTTP-blind layer can
157
+ # see it. `JSON::ParserError` already names "this wasn't JSON" exactly
158
+ # right ; wrapping it in a bespoke `JsonDoor`-specific class would
159
+ # only be a second name for the same fact, so it propagates exactly as
160
+ # `JSON.parse` raises it — a calling app catches it the same standard
161
+ # way it would catch any other malformed-input error, no new class to
162
+ # learn.
163
+ def parse(raw_json) = JSON.parse(raw_json)
164
+ end
165
+ end
166
+ end