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,300 @@
1
+ require "json"
2
+ require "rack"
3
+ require_relative "page"
4
+ require_relative "field_shape"
5
+ require_relative "index_renderer"
6
+ require_relative "record_renderer"
7
+ require_relative "command_form_renderer"
8
+ require_relative "query_form_renderer"
9
+ require_relative "params"
10
+ require_relative "../facade/command_request"
11
+ require_relative "../facade/json_door"
12
+
13
+ module Hecks
14
+ module Forms
15
+ # The content-negotiated router: `GET /Banking/Account/Overdrawn.html`
16
+ # renders the query view built in this directory; the identical path
17
+ # with no extension (or `.json`) dispatches the SAME ask through
18
+ # `Runtime::Dispatcher#query` and answers with its raw result — one
19
+ # route, two representations, exactly the "change the file format on
20
+ # the route" mechanism this was built around
21
+ # (docs/command-form-and-query-form-bluebook.md).
22
+ #
23
+ # A plain Rack app (`#call(env)`) — no Sinatra, no Rails. `rack` itself
24
+ # is a LAZY dependency the same way `pg`/`oauth2`/`aws-sdk-lambda` are
25
+ # for their own adapters (see the Gemfile's own comment) : a project
26
+ # that never boots this file never needs it installed.
27
+ class App
28
+ def self.for(registry:, app_name:)
29
+ config = Forms.config(app_name) ||
30
+ raise(ArgumentError, "no app #{app_name.inspect} configured — " \
31
+ "call Hecks::Forms.configure(#{app_name.inspect}) { expose \"...\" } first")
32
+ new(registry: registry, exposed: config.exposes)
33
+ end
34
+
35
+ def initialize(registry:, exposed:, dispatcher: nil)
36
+ @registry = registry
37
+ @exposed = exposed
38
+ @dispatcher = dispatcher || Runtime::Dispatcher.new(registry)
39
+ end
40
+
41
+ def call(env)
42
+ request = Rack::Request.new(env)
43
+ route(request)
44
+ rescue RouteNotFound => e
45
+ respond(404, "text/plain", e.message)
46
+ end
47
+
48
+ class RouteNotFound < StandardError; end
49
+
50
+ private
51
+
52
+ def route(request)
53
+ segments = request.path_info.split("/").reject(&:empty?)
54
+ return home(request) if segments.empty?
55
+
56
+ domain = segments[0]
57
+ refuse_unexposed(domain)
58
+ chapter = @registry.bluebook(domain) || raise(RouteNotFound, "no domain #{domain.inspect} loaded")
59
+
60
+ return aggregate_route(request, chapter, *split_format(segments[1])) if segments.size == 2
61
+
62
+ return verb_or_record_route(request, chapter, segments[1], *split_format(segments[2])) if segments.size == 3
63
+
64
+ if segments.size == 4
65
+ aggregate = chapter.aggregate(segments[1])
66
+ entity = aggregate&.entities&.find { |candidate| candidate.hecks_name == segments[2] }
67
+ entity_command = entity&.command(split_format(segments[3]).first)
68
+ if entity_command
69
+ raise RouteNotFound,
70
+ "entity command routes are not supported by Forms; use a command door with " \
71
+ "to.aggregate and to.entity"
72
+ end
73
+ end
74
+
75
+ respond(404, "text/plain", "no route for #{request.path_info}")
76
+ end
77
+
78
+ def refuse_unexposed(domain)
79
+ return if @exposed.include?(domain)
80
+
81
+ raise RouteNotFound, "#{domain.inspect} is not exposed by this app — declared chapters: #{@exposed.join(', ')}"
82
+ end
83
+
84
+ def split_format(segment)
85
+ name, format = segment.to_s.split(".", 2)
86
+ [name, format || "json"]
87
+ end
88
+
89
+ # ---- home -------------------------------------------------------
90
+
91
+ def home(request)
92
+ format = request.params["format"] || "html"
93
+ chapters = @exposed.to_h { |name| [name, @registry.bluebook(name)] }.compact
94
+ return json(200, chapters.transform_values { |c| { aggregates: c.aggregates.map(&:hecks_name) } }) if format != "html"
95
+
96
+ html("Exposed domains", IndexRenderer.render(chapters))
97
+ end
98
+
99
+ # ---- /:domain/:aggregate.:format ---------------------------------
100
+
101
+ def aggregate_route(request, chapter, aggregate_name, format)
102
+ aggregate = find_aggregate(chapter, aggregate_name)
103
+ return respond(405, "text/plain", "GET only") unless request.get?
104
+
105
+ if format == "html"
106
+ html("#{chapter.name}::#{aggregate.hecks_name}", RecordRenderer.index(registry: @registry, domain: chapter.name, aggregate: aggregate),
107
+ breadcrumbs: [[chapter.name, "/"], [aggregate.hecks_name, nil]])
108
+ else
109
+ instances = @registry.repository(chapter.name, aggregate).all
110
+ # id LAST — see Instance#to_h's own comment: an aggregate free
111
+ # to declare its own attribute literally named `id` has that
112
+ # attribute's own wrapped value sitting in `i.state[:id]`
113
+ # already, which used to silently clobber the correct bare
114
+ # identity when merged first.
115
+ json(200, instances.map { |i| i.state.merge(id: i.id) })
116
+ end
117
+ end
118
+
119
+ # ---- /:domain/:aggregate/:verb_or_id.:format ----------------------
120
+
121
+ def verb_or_record_route(request, chapter, aggregate_name, verb_or_id, format)
122
+ aggregate = find_aggregate(chapter, aggregate_name)
123
+ domain = chapter.name
124
+
125
+ if (command = aggregate.command(verb_or_id))
126
+ return command_route(request, domain, aggregate, command, format)
127
+ end
128
+
129
+ if (query = aggregate.query(verb_or_id))
130
+ return query_route(request, domain, aggregate, query, format)
131
+ end
132
+
133
+ record_route(request, domain, aggregate, verb_or_id, format)
134
+ end
135
+
136
+ def command_route(request, domain, aggregate, command, format)
137
+ action = "/#{domain}/#{aggregate.hecks_name}/#{command.hecks_name}.html"
138
+ return command_json(request, domain, aggregate, command) if format != "html"
139
+ if request.get?
140
+ return command_form(domain, aggregate, command, action,
141
+ prefill: receiver_compatibility(request.GET, command))
142
+ end
143
+ return respond(405, "text/plain", "GET or POST only") unless request.post?
144
+
145
+ submit_command(request, domain, aggregate, command, action)
146
+ end
147
+
148
+ def command_form(domain, aggregate, command, action, status: 200, values: nil, error: nil, prefill: {})
149
+ html("#{domain}::#{aggregate.hecks_name}.#{command.hecks_name}",
150
+ CommandFormRenderer.render(registry: @registry, domain: domain, aggregate: aggregate, command: command,
151
+ action: action, values: values, error: error, prefill: prefill),
152
+ breadcrumbs: [[domain, "/"], [aggregate.hecks_name, "/#{domain}/#{aggregate.hecks_name}.html"],
153
+ [command.hecks_name, nil]],
154
+ status: status)
155
+ end
156
+
157
+ def submit_command(request, domain, aggregate, command, action)
158
+ raw, envelope = submitted_command(request, aggregate, command)
159
+ result = @dispatcher.dispatch("#{domain}::#{aggregate.hecks_name}.#{command.hecks_name}", **envelope)
160
+ redirect("/#{domain}/#{aggregate.hecks_name}/#{result.id}.html")
161
+ rescue *Runtime::DOMAIN_REFUSALS, ArgumentError, TypeError, JSON::ParserError => e
162
+ status = e.is_a?(Runtime::NotFound) ? 404 : 422
163
+ command_form(domain, aggregate, command, action, status: status, values: raw, error: e)
164
+ end
165
+
166
+ def command_json(request, domain, aggregate, command)
167
+ return json(200, command.to_h) if request.get? && request.GET.reject { |k, _| k == "format" }.empty?
168
+ return respond(405, "text/plain", "GET or POST only") unless request.post?
169
+
170
+ _, envelope = submitted_command(request, aggregate, command)
171
+ result = @dispatcher.dispatch("#{domain}::#{aggregate.hecks_name}.#{command.hecks_name}", **envelope)
172
+ # id LAST — same reasoning as the other JSON-serializing call
173
+ # sites in this file (see aggregate_route's own comment).
174
+ json(201, result.state.merge(id: result.id))
175
+ rescue *Runtime::DOMAIN_REFUSALS, ArgumentError, TypeError, JSON::ParserError => e
176
+ status = e.is_a?(Runtime::NotFound) ? 404 : 422
177
+ json(status, { error: e.class.name.split("::").last, message: e.message })
178
+ end
179
+
180
+ def query_route(request, domain, aggregate, query, format)
181
+ return respond(405, "text/plain", "GET only") unless request.get?
182
+
183
+ params = request.GET
184
+ asked = params.reject { |k, _| k == "format" }
185
+ return query_json(domain, aggregate, query, asked) if format != "html"
186
+
187
+ query_html(domain, aggregate, query, asked)
188
+ end
189
+
190
+ def query_html(domain, aggregate, query, asked)
191
+ action = "/#{domain}/#{aggregate.hecks_name}/#{query.hecks_name}.html"
192
+ fields = query.attributes.map { |a| FieldShape.resolve(a, aggregate: aggregate) }
193
+ results, error = asked.empty? ? [nil, nil] : run_query(domain, aggregate, query, fields, asked)
194
+ html("#{domain}::#{aggregate.hecks_name}.#{query.hecks_name}",
195
+ QueryFormRenderer.render(registry: @registry, domain: domain, aggregate: aggregate, query: query,
196
+ action: action, params: asked, results: results, error: error),
197
+ breadcrumbs: [[domain, "/"], [aggregate.hecks_name, "/#{domain}/#{aggregate.hecks_name}.html"],
198
+ [query.hecks_name, nil]],
199
+ status: error ? 422 : 200)
200
+ end
201
+
202
+ def query_json(domain, aggregate, query, asked)
203
+ return json(200, query.to_h) if asked.empty?
204
+
205
+ fields = query.attributes.map { |a| FieldShape.resolve(a, aggregate: aggregate) }
206
+ results, error = run_query(domain, aggregate, query, fields, asked)
207
+ return json(422, { error: error.class.name.split("::").last, message: error.message }) if error
208
+
209
+ # id LAST — same reasoning as the other JSON-serializing call
210
+ # sites in this file (see aggregate_route's own comment).
211
+ json(200, results.map { |i| i.state.merge(id: i.id) })
212
+ end
213
+
214
+ # `Dispatcher#query` answers an array of plain hashes
215
+ # (`{id:}.merge(state)`, flattened by `QueryInterpreter#call` on
216
+ # every path it can take — native pushdown or the in-memory
217
+ # fallback alike), not `Runtime::Instance`. Wrapped into `Record`
218
+ # here, once, so every renderer downstream reads one shape whether
219
+ # its records came from a query or straight off a repository.
220
+ def run_query(domain, aggregate, query, fields, asked)
221
+ args = Params.extract(fields, asked)
222
+ rows = @dispatcher.query("#{domain}::#{aggregate.hecks_name}.#{query.hecks_name}", **args)
223
+ [rows.map { |row| Record.new(row[:id], row.reject { |k, _| k == :id }) }, nil]
224
+ rescue *Runtime::DOMAIN_REFUSALS, ArgumentError, TypeError => e
225
+ [nil, e]
226
+ end
227
+
228
+ def record_route(request, domain, aggregate, id, format)
229
+ return respond(405, "text/plain", "GET only") unless request.get?
230
+
231
+ instance = @registry.repository(domain, aggregate).find(id)
232
+ return not_found(aggregate, id, format) unless instance
233
+ # id LAST — same reasoning as the other JSON-serializing call
234
+ # sites in this file (see aggregate_route's own comment).
235
+ return json(200, instance.state.merge(id: instance.id)) if format != "html"
236
+
237
+ html("#{domain}::#{aggregate.hecks_name} #{id}", RecordRenderer.show(registry: @registry, domain: domain, aggregate: aggregate, id: id),
238
+ breadcrumbs: [[domain, "/"], [aggregate.hecks_name, "/#{domain}/#{aggregate.hecks_name}.html"], [id, nil]])
239
+ end
240
+
241
+ def not_found(aggregate, id, format)
242
+ return json(404, { error: "NotFound", message: "no #{aggregate.hecks_name} #{id}" }) if format != "html"
243
+
244
+ respond(404, "text/html; charset=utf-8",
245
+ Page.render(title: "not found", body: "<h1>Not found</h1><p>No #{Escape.html(aggregate.hecks_name)} #{Escape.html(id)}.</p>"))
246
+ end
247
+
248
+ def find_aggregate(chapter, name)
249
+ chapter.aggregate(name) || raise(RouteNotFound, "#{chapter.name} has no aggregate #{name.inspect}")
250
+ end
251
+
252
+ def command_envelope(command, args)
253
+ Facade::CommandRequest.normalize(args, receiver: command_receiver(command), legacy_receiver: :id)
254
+ end
255
+
256
+ def command_receiver(command) = command.creates? ? nil : :aggregate
257
+
258
+ def submitted_command(request, aggregate, command)
259
+ if request.media_type == "application/json"
260
+ raw = Facade::JsonDoor.parse(request.body.read)
261
+ envelope = Facade::JsonDoor.command_request(raw, receiver: command_receiver(command),
262
+ legacy_receiver: :id)
263
+ return [raw, envelope]
264
+ end
265
+
266
+ raw = receiver_compatibility(request.POST, command)
267
+ fields = CommandFormRenderer.fields_for(aggregate, command)
268
+ args = Params.extract(fields, raw)
269
+ [raw, command_envelope(command, args)]
270
+ end
271
+
272
+ # Existing integrations may still submit id. It remains an accepted
273
+ # edge spelling, but new forms expose only the receiver name to.
274
+ def receiver_compatibility(raw, command)
275
+ return raw if command.creates?
276
+ return raw unless raw.key?("id") && !raw.key?("to")
277
+
278
+ raw.merge("to" => raw["id"])
279
+ end
280
+
281
+ # ---- rendering ----------------------------------------------------
282
+
283
+ def html(title, body, breadcrumbs: [], status: 200)
284
+ respond(status, "text/html; charset=utf-8", Page.render(title: title, body: body, breadcrumbs: breadcrumbs))
285
+ end
286
+
287
+ def json(status, payload)
288
+ respond(status, "application/json", JSON.pretty_generate(payload))
289
+ end
290
+
291
+ def redirect(location)
292
+ [302, { "location" => location }, []]
293
+ end
294
+
295
+ def respond(status, content_type, body)
296
+ [status, { "content-type" => content_type }, [body]]
297
+ end
298
+ end
299
+ end
300
+ end
@@ -0,0 +1,113 @@
1
+ require "json"
2
+ require_relative "html"
3
+ require_relative "field_shape"
4
+ require_relative "field_renderer"
5
+ require_relative "reference_options"
6
+ require_relative "params"
7
+
8
+ module Hecks
9
+ module Forms
10
+ # Command -> the page body for its HTML form — the working half of
11
+ # what `command_form.bluebook` names (see docs/command-form-and-query-
12
+ # form-bluebook.md). Every command in every loaded, exposed bluebook
13
+ # renders through here — nothing here is per-command code; see that
14
+ # doc for the 1:1 rule this deliberately holds to for now.
15
+ module CommandFormRenderer
16
+ # `registry` is only used to populate a `:reference` field's
17
+ # `<select>` with real records (including the identity picker itself,
18
+ # for a non-creating command — see `identity_field` below). `values`/
19
+ # `error` carry a sticky re-render after a refused submission; leave
20
+ # both nil/`{}` for a fresh form. `prefill` carries values a caller
21
+ # arrived WITH (typically `?to=...` off a record's own detail page) —
22
+ # kept separate from `values` because a prefill is not an error retry
23
+ # and should not be treated as one by a future reader of this code.
24
+ # The SAME field list a POST handler needs to cast raw params against
25
+ # (params.rb's `Params.extract`) — one derivation, so a page never
26
+ # renders an input the submit handler doesn't also expect.
27
+ def self.fields_for(aggregate, command)
28
+ addressing = command.creates? ? [] : [identity_field(aggregate)]
29
+ addressing + command.attributes.map { |a| FieldShape.resolve(a, aggregate: aggregate) }
30
+ end
31
+
32
+ def self.render(registry:, domain:, aggregate:, command:, action:, values: nil, error: nil, prefill: {})
33
+ all_fields = fields_for(aggregate, command)
34
+ reference_options = ReferenceOptions.collect(registry, domain, all_fields)
35
+ shown_values = values || prefill
36
+
37
+ <<~HTML
38
+ #{header(domain, aggregate, command)}
39
+ #{error_banner(error)}
40
+ <form method="post" action="#{Escape.attr(action)}" novalidate>
41
+ #{all_fields.map { |f| FieldRenderer.render(f, values: shown_values, errors: field_errors(error), reference_options: reference_options) }.join}
42
+ <div class="actions">
43
+ <button type="submit">#{Escape.html(command.hecks_name)}</button>
44
+ <a class="button secondary" href="#{Escape.attr("/#{domain}/#{aggregate.hecks_name}")}">Cancel</a>
45
+ </div>
46
+ </form>
47
+ #{inspect_panel(domain, aggregate, command, action, all_fields)}
48
+ HTML
49
+ end
50
+
51
+ def self.identity_field(aggregate)
52
+ Field.new(path: "to", label: "#{aggregate.hecks_name} (#{aggregate.identity_paths.join(', ')})",
53
+ kind: :reference, html_type: "text", target_aggregate: aggregate,
54
+ help: "The record this command acts on.")
55
+ end
56
+
57
+ def self.header(domain, aggregate, command)
58
+ <<~HTML
59
+ <h1>#{Escape.html("#{domain}::#{aggregate.hecks_name}.#{command.hecks_name}")}</h1>
60
+ #{command.role ? %(<span class="badge role">role: #{Escape.html(command.role)}</span>) : ''}
61
+ #{command.creates? ? %(<span class="badge">creates a new #{Escape.html(aggregate.hecks_name)}</span>) : ''}
62
+ #{command.goal ? %(<p class="goal">#{Escape.html(command.goal)}</p>) : ''}
63
+ #{givens_callout(command)}
64
+ HTML
65
+ end
66
+
67
+ def self.givens_callout(command)
68
+ return "" if command.givens.empty?
69
+
70
+ items = command.givens.map { |given| "<li>#{Escape.html(given.description)}</li>" }
71
+ %(<div class="callout"><strong>Preconditions</strong> — refused if any fail:<ul>#{items.join}</ul></div>)
72
+ end
73
+
74
+ def self.error_banner(error)
75
+ return "" unless error
76
+
77
+ <<~HTML
78
+ <div class="error-banner" role="alert">
79
+ <p><strong>#{Escape.html(error.class.name.split('::').last)}</strong> — #{Escape.html(error.message)}</p>
80
+ </div>
81
+ HTML
82
+ end
83
+
84
+ # No structured field attribution exists on a domain refusal today
85
+ # (it is a typed exception with a rendered message — see
86
+ # docs/command-form-and-query-form-bluebook.md's note on
87
+ # `RefusalWording`), so this returns empty rather than guessing which
88
+ # field a message meant; the banner above carries the real text
89
+ # instead of a misattributed hint.
90
+ def self.field_errors(_error) = {}
91
+
92
+ def self.inspect_panel(domain, aggregate, command, action, fields)
93
+ verb = "#{domain}::#{aggregate.hecks_name}.#{command.hecks_name}"
94
+ paths = Params.paths(fields)
95
+ curl = <<~SH.strip
96
+ curl -X POST '#{action}' \\
97
+ #{paths.map { |path| "-d '#{path}=...'" }.join(" \\\n ")}
98
+ SH
99
+ <<~HTML
100
+ <details class="inspect">
101
+ <summary>Inspect — #{Escape.html(verb)}</summary>
102
+ <p>Emits: #{command.emits.empty? ? '<em>nothing declared</em>' : command.emits.map { |e| "<code>#{Escape.html(e)}</code>" }.join(', ')}</p>
103
+ <p>Equivalent request (as <code>curl</code>) — every field is <code>#{Escape.html('name.path')}</code>-encoded, form or JSON alike:</p>
104
+ <div class="link-row"><code id="curl-snippet">#{Escape.html(curl)}</code><button type="button" class="copy" data-copy="#curl-snippet">copy</button></div>
105
+ <p>Fields this command takes: #{paths.map { |p| "<code>#{Escape.html(p)}</code>" }.join(', ')}</p>
106
+ <p>The command's own declaration, as the runtime holds it:</p>
107
+ <pre>#{Escape.html(JSON.pretty_generate(command.to_h))}</pre>
108
+ </details>
109
+ HTML
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,3 @@
1
+ Hecks::Forms.configure "BankingConsole" do
2
+ expose "Banking"
3
+ end
@@ -0,0 +1,170 @@
1
+ require_relative "html"
2
+ require_relative "field_shape"
3
+ require_relative "../naming"
4
+
5
+ module Hecks
6
+ module Forms
7
+ # A Field (field_shape.rb) -> the `<div class="field">...</div>` or
8
+ # `<fieldset>...</fieldset>` markup for it. One renderer, called
9
+ # recursively for a group's children, shared by CommandFormRenderer
10
+ # (POST) and QueryFormRenderer (GET) — the same attribute shape asks
11
+ # for the same input either way; only the surrounding `<form>`'s
12
+ # method differs.
13
+ module FieldRenderer
14
+ # `values` is the nested hash a sticky re-render (a rejected command,
15
+ # a submitted query) carries back — read with the SAME dotted path a
16
+ # Field already carries, via `dig`. `reference_options` maps a
17
+ # `:reference` field's own path to `[[id, label], ...]`, built by the
18
+ # caller (it needs a repository; this module stays pure markup).
19
+ def self.render(field, values: {}, errors: nil, reference_options: {})
20
+ case field.kind
21
+ when :group then group(field, values, errors, reference_options, tag: "fieldset")
22
+ when :money then group(field, values, errors, reference_options, tag: "fieldset", css: "money")
23
+ when :list then list(field, values, errors)
24
+ else leaf(field, values, errors, reference_options)
25
+ end
26
+ end
27
+
28
+ def self.group(field, values, errors, reference_options, tag:, css: nil)
29
+ inner = field.children.map { |child| render(child, values: values, errors: errors, reference_options: reference_options) }
30
+ <<~HTML
31
+ <#{tag}#{css ? %( class="#{css}") : ""}>
32
+ <legend>#{Escape.html(field.label)}#{required_mark(field)}</legend>
33
+ #{inner.join("\n")}
34
+ </#{tag}>
35
+ HTML
36
+ end
37
+
38
+ def self.list(field, values, _errors)
39
+ item = field.children.first
40
+ current = Array(dig(values, field.path)).join("\n")
41
+ <<~HTML
42
+ <div class="field">
43
+ <label for="#{dom_id(field.path)}">#{Escape.html(field.label)}#{required_mark(field)}</label>
44
+ <textarea id="#{dom_id(field.path)}" name="#{Escape.attr(field.path)}"
45
+ #{aria(field)} placeholder="one #{Escape.attr(item.label.downcase)} per line">#{Escape.html(current)}</textarea>
46
+ <span class="help" id="#{dom_id(field.path)}-help">One #{Escape.html(item.label.downcase)} per line#{item.leaf? ? '' : ' — each line is one JSON object'}.</span>
47
+ </div>
48
+ HTML
49
+ end
50
+
51
+ def self.leaf(field, values, errors, reference_options)
52
+ value = dig(values, field.path)
53
+ error = errors && errors[field.path]
54
+ body = case field.kind
55
+ when :boolean then checkbox(field, value)
56
+ when :radio then radio_group(field, value)
57
+ when :select then select(field, value)
58
+ when :textarea then textarea(field, value)
59
+ when :reference then reference_select(field, value, reference_options[field.path])
60
+ else input(field, value)
61
+ end
62
+ wrap(field, body, error)
63
+ end
64
+
65
+ def self.wrap(field, body, error)
66
+ <<~HTML
67
+ <div class="field#{error ? ' has-error' : ''}">
68
+ #{field.kind == :boolean ? '' : %(<label for="#{dom_id(field.path)}">#{Escape.html(field.label)}#{required_mark(field)}</label>)}
69
+ #{body}
70
+ #{field.help ? %(<span class="help" id="#{dom_id(field.path)}-help">#{Escape.html(field.help)}</span>) : ''}
71
+ #{error ? %(<span class="help" role="alert">#{Escape.html(error)}</span>) : ''}
72
+ </div>
73
+ HTML
74
+ end
75
+
76
+ def self.input(field, value)
77
+ money = field.kind == :number && field.help.to_s.include?("cents")
78
+ tag = Tag.void("input", id: dom_id(field.path), name: field.path, type: field.html_type,
79
+ value: value || field.default, step: field.step, pattern: leaf_pattern(field),
80
+ placeholder: field.default, **aria_attrs(field),
81
+ **(money ? { data_money_cents: money_preview_target(field) } : {}))
82
+ return tag unless money
83
+
84
+ %(#{tag} <span id="#{dom_id(field.path)}-preview" class="mono help" aria-live="polite"></span>)
85
+ end
86
+
87
+ def self.textarea(field, value)
88
+ %(<textarea id="#{dom_id(field.path)}" name="#{Escape.attr(field.path)}" #{aria(field)}>#{Escape.html(value)}</textarea>)
89
+ end
90
+
91
+ def self.checkbox(field, value)
92
+ checked = value.nil? ? field.default == true : [true, "true", "on", "1"].include?(value)
93
+ <<~HTML
94
+ <div class="checkbox-row">
95
+ <input type="hidden" name="#{Escape.attr(field.path)}" value="0">
96
+ #{Tag.void('input', id: dom_id(field.path), name: field.path, type: 'checkbox', value: '1', checked: checked)}
97
+ <label for="#{dom_id(field.path)}">#{Escape.html(field.label)}</label>
98
+ </div>
99
+ HTML
100
+ end
101
+
102
+ def self.radio_group(field, value)
103
+ selected = value || field.default
104
+ options = field.options.map do |option_value, option_label|
105
+ checked = option_value.to_s == selected.to_s
106
+ id = "#{dom_id(field.path)}-#{Naming.snake(option_value)}"
107
+ <<~HTML
108
+ <label>#{Tag.void('input', id: id, type: 'radio', name: field.path, value: option_value, checked: checked)} #{Escape.html(option_label)}</label>
109
+ HTML
110
+ end
111
+ %(<div class="radio-group" role="radiogroup">#{options.join}</div>)
112
+ end
113
+
114
+ def self.select(field, value)
115
+ selected = value || field.default
116
+ options = field.options.map do |option_value, option_label|
117
+ %(<option value="#{Escape.attr(option_value)}"#{option_value.to_s == selected.to_s ? ' selected' : ''}>#{Escape.html(option_label)}</option>)
118
+ end
119
+ blank = field.optional? ? %(<option value="">—</option>) : ""
120
+ %(<select id="#{dom_id(field.path)}" name="#{Escape.attr(field.path)}" #{aria(field)}>#{blank}#{options.join}</select>)
121
+ end
122
+
123
+ def self.reference_select(field, value, options)
124
+ return input(field.tap { |f| f.html_type = "text" }, value) unless options && !options.empty?
125
+
126
+ rendered = options.map do |id, label|
127
+ %(<option value="#{Escape.attr(id)}"#{id.to_s == value.to_s ? ' selected' : ''}>#{Escape.html(label)}</option>)
128
+ end
129
+ blank = field.optional? ? %(<option value="">—</option>) : %(<option value="" disabled#{value ? '' : ' selected'}>choose one…</option>)
130
+ %(<select id="#{dom_id(field.path)}" name="#{Escape.attr(field.path)}" #{aria(field)}>#{blank}#{rendered.join}</select>)
131
+ end
132
+
133
+ def self.required_mark(field) = field.required? ? %(<span class="required-mark" title="required">*</span>) : ""
134
+ def self.dom_id(path) = "f-#{path.to_s.tr('.', '-')}"
135
+ def self.leaf_pattern(field) = field.kind == :reference ? nil : field.pattern
136
+ def self.money_preview_target(field) = "##{dom_id(field.path)}-preview"
137
+
138
+ def self.aria(field)
139
+ Tag.attrs(required: field.required?, aria_describedby: field.help ? "#{dom_id(field.path)}-help" : nil)
140
+ end
141
+
142
+ def self.aria_attrs(field)
143
+ { required: field.required?, aria_describedby: field.help ? "#{dom_id(field.path)}-help" : nil }
144
+ end
145
+
146
+ # `values` is a nested hash; `path` a dotted string using the SAME
147
+ # segment spelling the hash keys are built from (`leaf_key` in
148
+ # params.rb) — symbols one level down from a group, strings at the
149
+ # flat top when a sticky POST re-render hands raw params back untouched.
150
+ def self.dig(values, path)
151
+ return nil unless values.is_a?(Hash)
152
+
153
+ # A sticky re-render after a refused submission hands back the RAW
154
+ # flat params (`{"amount.cents"=>"1050"}` — the same shape the form
155
+ # posted, string values and all, dotted key intact) ; a prefill from
156
+ # an existing record's own state hands back a NESTED hash instead
157
+ # (`{amount: {cents: 1050}}`). Flat wins when both would answer,
158
+ # since only the raw form is ever what the caller actually typed.
159
+ flat = values[path.to_s] || values[path.to_sym]
160
+ return flat unless flat.nil?
161
+
162
+ path.to_s.split(".").reduce(values) do |acc, segment|
163
+ break nil unless acc.is_a?(Hash)
164
+
165
+ acc[segment.to_sym] || acc[segment]
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end