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,232 @@
1
+ require_relative "../bluebook/attribute"
2
+ require_relative "value_object_shape"
3
+
4
+ module Hecks
5
+ module Forms
6
+ # One resolved field, ready for a renderer to turn into markup. A LEAF
7
+ # scalar carries `kind`/`options`/etc; a `:group` or `:list` carries
8
+ # `children` instead and nothing else on itself.
9
+ #
10
+ # `path` is the DOTTED field path this attribute is reached at —
11
+ # `"email.address"`, `"amount.cents"` — the same convention the language
12
+ # already uses for a query's own cross-object `where` clauses
13
+ # (`where(:"customer.status" => ...)`), not a fresh one invented here. A
14
+ # command receives its arguments as nested hashes
15
+ # (`email: {address: "..."}`), so `Params.unflatten` (params.rb) walks
16
+ # this same dotted spelling back apart on submit — one convention, two
17
+ # directions.
18
+ Field = Struct.new(
19
+ :path, :label, :kind, :html_type, :options, :children, :default, :optional,
20
+ :pattern, :step, :help, :target_aggregate, keyword_init: true
21
+ ) do
22
+ def leaf? = kind != :group && kind != :list
23
+ def optional? = optional
24
+ def required? = !optional
25
+ end
26
+
27
+ module Humanize
28
+ # "given" -> "Given", "daily_limit" -> "Daily limit", "end_to_end" ->
29
+ # "End to end" — the label a plain reader wants, not the wire spelling.
30
+ def self.label(text)
31
+ # Split on "." FIRST, alone, to take only the last path segment —
32
+ # "daily_limit" is one segment (the underscore is a word break
33
+ # inside it, not a path hop) and must keep both its words; only a
34
+ # genuinely dotted path ("amount.cents") drops everything before
35
+ # the last ".".
36
+ segment = text.to_s.split(".").last.to_s
37
+ words = segment.split("_")
38
+ return segment if words.empty?
39
+
40
+ ([words.first.capitalize] + words.drop(1)).join(" ")
41
+ end
42
+
43
+ # The full dotted path, each segment humanized and joined with
44
+ # " → " for a fieldset legend spanning more than one hop
45
+ # ("Amount → Cents") — a group's own legend, not a leaf's label.
46
+ def self.breadcrumb(path)
47
+ path.to_s.split(".").map { |part| label(part) }.join(" → ")
48
+ end
49
+ end
50
+
51
+ # Attribute -> Field. The one mapping every renderer in this
52
+ # directory reads instead of re-deriving its own — see
53
+ # docs/command-form-and-query-form-bluebook.md's survey of the gap this closes (the
54
+ # prior generic-console attempt fell back to `type="text"` for anything
55
+ # that wasn't a number or an enum; a `pattern` naming an email shape, or a
56
+ # closed set another aggregate declares, went unread).
57
+ module FieldShape
58
+ PRIMITIVES = Bluebook::Attribute::PRIMITIVES
59
+
60
+ # `aggregate:` is the Aggregate that OWNS this attribute (a
61
+ # command's, a query's, or — recursively — a value object's) —
62
+ # needed to resolve `reference_to`, `admits:`, and a same-chapter
63
+ # value object by name. `path:` defaults to the attribute's own name;
64
+ # a caller resolving a nested value object's attribute passes the
65
+ # dotted path so far.
66
+ def self.resolve(attribute, aggregate:, path: attribute.name.to_s)
67
+ return list_field(attribute, aggregate, path) if attribute.list?
68
+
69
+ common = { path: path, label: Humanize.label(path), default: attribute.default,
70
+ optional: attribute.optional?, pattern: attribute.pattern }
71
+
72
+ return reference_field(attribute, common) if attribute.reference?
73
+ return admitted_field(attribute, aggregate, common) if attribute.admits
74
+ return value_object_field(attribute, aggregate, common) unless PRIMITIVES.include?(attribute.type)
75
+
76
+ primitive_field(attribute, common)
77
+ end
78
+
79
+ def self.list_field(attribute, aggregate, path)
80
+ # The scalar shape one element of this list would take, so a
81
+ # renderer can say what belongs on each line without a second
82
+ # mapping table. `list:` is the only thing that differs.
83
+ scalar = Bluebook::Attribute.new(
84
+ name: attribute.name, type: attribute.type, list: false,
85
+ default: nil, optional: true, pattern: attribute.pattern, admits: attribute.admits
86
+ )
87
+ Field.new(path: path, label: Humanize.label(path), kind: :list, optional: attribute.optional?,
88
+ children: [resolve(scalar, aggregate: aggregate, path: path)])
89
+ end
90
+
91
+ def self.reference_field(attribute, common)
92
+ target = attribute.type.resolve
93
+ Field.new(**common, kind: :reference, html_type: "text", target_aggregate: target,
94
+ help: target ? "References an existing #{target.hecks_name} by id." :
95
+ "References an aggregate in another domain — enter its id.")
96
+ end
97
+
98
+ # `admits:` names a closed set declared ELSEWHERE (`"Account::
99
+ # LedgerDirection"`) that the value must belong to — see
100
+ # Runtime::Value::Admission#admitted_members, which this mirrors
101
+ # exactly (same split, same chapter walk, same discriminant rule) so
102
+ # a rendered `<select>` never offers a member the runtime would then
103
+ # refuse.
104
+ def self.admitted_field(attribute, aggregate, common)
105
+ set_aggregate_name, set_name = attribute.admits.to_s.split("::", 2)
106
+ chapter = aggregate.hecks_owner
107
+ set = set_name && chapter&.aggregate(set_aggregate_name)&.value_object(set_name)
108
+ return primitive_field(attribute, common) unless set # undeclared — refuse-at-dispatch stays the backstop
109
+
110
+ options = select_or_radio(common, closed_set_options(set))
111
+ # The attribute's OWN type still has to be built the shape coercion
112
+ # expects (`Value::Coercion#fields_for` refuses anything that
113
+ # isn't a Hash or a Value for a value-object-typed attribute) — a
114
+ # plain String attribute stays a bare scalar, but a value object
115
+ # like `MovementDirection { value }` still needs the ".value" hop
116
+ # even though the SET it's checked against (`admits:`) is declared
117
+ # somewhere else entirely. Same unwrap `value_object_field` does,
118
+ # kept separate because an admitted set changes the OPTIONS, not
119
+ # which field the hop lands on.
120
+ own_shape = own_value_object(attribute, aggregate)
121
+ inner = own_shape && ValueObjectShape.sole_attribute(own_shape)
122
+ return options unless inner
123
+
124
+ options.path = "#{common[:path]}.#{inner.name}"
125
+ options
126
+ end
127
+
128
+ def self.own_value_object(attribute, aggregate)
129
+ aggregate.value_object(attribute.type) || cross_aggregate_value_object(aggregate, attribute.type)
130
+ end
131
+
132
+ def self.value_object_field(attribute, aggregate, common)
133
+ shape = own_value_object(attribute, aggregate)
134
+ return primitive_field(attribute, common) unless shape
135
+
136
+ return closed_set_field(shape, common) if shape.closed_set?
137
+ return money_field(shape, common) if ValueObjectShape.money?(shape)
138
+
139
+ # A single-attribute value object (EmailAddress{address}, CustomerNumber{value})
140
+ # is a NAME for a scalar, not a genuine group — unwrap it so the form
141
+ # asks for one thing ("Email address") instead of a one-item fieldset,
142
+ # and so the inner attribute's OWN pattern (the real email regex,
143
+ # declared on `address`, not on the outer `email` attribute) drives
144
+ # the input type. [[feedback_name_the_scalar_field]] says the same
145
+ # thing about Ruby call sites; a form asks the identical question.
146
+ if (inner = ValueObjectShape.sole_attribute(shape))
147
+ return resolve(inner, aggregate: aggregate, path: "#{common[:path]}.#{inner.name}")
148
+ .tap { |field| field.optional = common[:optional] || field.optional }
149
+ end
150
+
151
+ group_field(shape, aggregate, common)
152
+ end
153
+
154
+ def self.cross_aggregate_value_object(aggregate, type_name)
155
+ aggregate.hecks_owner&.aggregates&.each do |sibling|
156
+ found = sibling.value_object(type_name)
157
+ return found if found
158
+ end
159
+ nil
160
+ end
161
+
162
+ def self.group_field(shape, aggregate, common)
163
+ children = shape.attributes.map { |inner| resolve(inner, aggregate: aggregate, path: "#{common[:path]}.#{inner.name}") }
164
+ Field.new(path: common[:path], label: common[:label], kind: :group, optional: common[:optional], children: children)
165
+ end
166
+
167
+ def self.money_field(shape, common)
168
+ cents = Field.new(path: "#{common[:path]}.cents", label: "Amount (cents)", kind: :number,
169
+ html_type: "number", step: "1", optional: common[:optional],
170
+ default: shape.attribute(:cents)&.default, help: "Whole cents — 1050 is $10.50.")
171
+ currency = Field.new(path: "#{common[:path]}.currency", label: "Currency", kind: :text, html_type: "text",
172
+ optional: true, default: shape.attribute(:currency)&.default || "USD",
173
+ help: "Three-letter code.")
174
+ Field.new(path: common[:path], label: common[:label], kind: :money, optional: common[:optional],
175
+ children: [cents, currency])
176
+ end
177
+
178
+ def self.closed_set_options(value_object)
179
+ discriminant = value_object.attributes.first.name
180
+ value_object.members.map { |member| member[discriminant].to_s }
181
+ end
182
+
183
+ # A `one_of` shape (`AccountKind{name}`, `LedgerDirection{value}`) is
184
+ # ALWAYS single-attribute in this language — the discriminant IS the
185
+ # whole value object — so the field's own path always gains that one
186
+ # hop; nothing to branch on the way `admitted_field` has to (a set
187
+ # named by `admits:` may sit on a multi-field value object it doesn't
188
+ # itself define the members of).
189
+ def self.closed_set_field(shape, common)
190
+ discriminant = shape.attributes.first.name
191
+ select_or_radio(common.merge(path: "#{common[:path]}.#{discriminant}"), closed_set_options(shape))
192
+ end
193
+
194
+ def self.select_or_radio(common, options)
195
+ kind = options.size <= 4 ? :radio : :select
196
+ Field.new(**common, kind: kind, html_type: "text", options: options.map { |value| [value, value] })
197
+ end
198
+
199
+ def self.primitive_field(attribute, common)
200
+ case attribute.type.to_s
201
+ when "Integer" then Field.new(**common, kind: :number, html_type: "number", step: "1")
202
+ when "Float" then Field.new(**common, kind: :number, html_type: "number", step: "any")
203
+ when "TrueClass", "FalseClass"
204
+ Field.new(**common, kind: :boolean, html_type: "checkbox")
205
+ else
206
+ text_field(attribute, common)
207
+ end
208
+ end
209
+
210
+ EMAIL_HINT = /email/i
211
+ URL_HINT = /\b(url|uri|website|link)\b/i
212
+ TEL_HINT = /phone|\btel(ephone)?\b/i
213
+ TEXTAREA_HINT = /\b(text|body|note|notes|description|message|comment)\b/i
214
+
215
+ def self.text_field(attribute, common)
216
+ name = attribute.name.to_s
217
+ pattern = attribute.pattern.to_s
218
+ html_type = if pattern.include?("@") || name.match?(EMAIL_HINT)
219
+ "email"
220
+ elsif pattern.match?(/https?/i) || name.match?(URL_HINT)
221
+ "url"
222
+ elsif name.match?(TEL_HINT)
223
+ "tel"
224
+ else
225
+ "text"
226
+ end
227
+ kind = html_type == "text" && name.match?(TEXTAREA_HINT) ? :textarea : :text
228
+ Field.new(**common, kind: kind, html_type: html_type)
229
+ end
230
+ end
231
+ end
232
+ end
@@ -0,0 +1,53 @@
1
+ module Hecks
2
+ module Forms
3
+ # Hand-rolled, on purpose — the repo has no ERB anywhere and no template
4
+ # engine dependency (see docs/command-form-and-query-form-bluebook.md's survey). Every
5
+ # other generator in this codebase (bin/reference's markdown, the IR's own
6
+ # `to_h`) builds output as plain Ruby strings; this does the same for HTML,
7
+ # with exactly one job: nothing that reaches `Escape.html` ever becomes a
8
+ # tag. A feature developer's own domain data — a customer's name, an
9
+ # account number a support rep typed into a form and got wrong — flows
10
+ # through here on every render, so escaping is not optional decoration.
11
+ module Escape
12
+ # Order matters — `&` first, or every escape this method itself just
13
+ # wrote (`&amp;`, `&lt;`, ...) gets re-escaped a second time.
14
+ def self.html(value)
15
+ value.to_s
16
+ .gsub("&", "&amp;")
17
+ .gsub("<", "&lt;")
18
+ .gsub(">", "&gt;")
19
+ .gsub('"', "&quot;")
20
+ .gsub("'", "&#39;")
21
+ end
22
+
23
+ # Safe inside a double-quoted HTML attribute specifically — `html`
24
+ # already covers this (it escapes `"`), kept as a named alias so a
25
+ # call site reads "this value fills an attribute" rather than repeating
26
+ # the same escaping and leaving the reader to check they match.
27
+ def self.attr(value) = html(value)
28
+ end
29
+
30
+ # A tiny attribute-hash -> string helper, shared by every renderer in
31
+ # this directory so `<input ...>` doesn't get hand-assembled five
32
+ # different ways with five different escaping bugs waiting in each.
33
+ # `true` renders as a bare boolean attribute (`required`, not
34
+ # `required="true"`); `nil`/`false` are dropped entirely.
35
+ module Tag
36
+ def self.attrs(pairs)
37
+ pairs.filter_map do |name, value|
38
+ next if value.nil? || value == false
39
+ next name.to_s.tr("_", "-") if value == true
40
+
41
+ %(#{name.to_s.tr("_", "-")}="#{Escape.attr(value)}")
42
+ end.join(" ")
43
+ end
44
+
45
+ def self.open(name, **pairs)
46
+ rendered = attrs(pairs)
47
+ rendered.empty? ? "<#{name}>" : "<#{name} #{rendered}>"
48
+ end
49
+
50
+ def self.void(name, **pairs) = open(name, **pairs) # self-closing tags read the same; HTML5 needs no slash
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,35 @@
1
+ require_relative "html"
2
+
3
+ module Hecks
4
+ module Forms
5
+ # The home page: every exposed chapter, every aggregate on it — the
6
+ # entry point into what would otherwise be a URL you'd have to already
7
+ # know. `chapters` is `{domain_name => Bluebook}`, in `expose` order
8
+ # (see config.rb).
9
+ module IndexRenderer
10
+ def self.render(chapters)
11
+ sections = chapters.map { |name, chapter| chapter_section(name, chapter) }
12
+ <<~HTML
13
+ <h1>Exposed domains</h1>
14
+ #{sections.join}
15
+ HTML
16
+ end
17
+
18
+ def self.chapter_section(name, chapter)
19
+ items = chapter.aggregates.map do |aggregate|
20
+ counts = "#{aggregate.commands.size} command#{'s' unless aggregate.commands.size == 1}, " \
21
+ "#{aggregate.queries.size} quer#{aggregate.queries.size == 1 ? 'y' : 'ies'}"
22
+ <<~HTML
23
+ <li><a href="/#{Escape.attr(name)}/#{Escape.attr(aggregate.hecks_name)}.html">
24
+ <span>#{Escape.html(aggregate.hecks_name)}</span><span class="kind">#{Escape.html(counts)}</span>
25
+ </a></li>
26
+ HTML
27
+ end
28
+ <<~HTML
29
+ <h2>#{Escape.html(name)}#{chapter.vision ? %( <span class="badge">#{Escape.html(chapter.vision)}</span>) : ''}</h2>
30
+ <ul class="verb-list">#{items.join}</ul>
31
+ HTML
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,157 @@
1
+ require_relative "html"
2
+
3
+ module Hecks
4
+ module Forms
5
+ # The one HTML shell every page in this app renders inside — nav,
6
+ # typography, form/table/badge styles, all inline (no CDN, no build
7
+ # step: this ships inside the `hecks` gem, and a project that
8
+ # embeds it gets one dependency-free response per request). Themed via
9
+ # `prefers-color-scheme` alone; nothing here reads a cookie or a query
10
+ # param for it, so it is never wrong for the browser rendering it.
11
+ module Page
12
+ def self.render(title:, body:, breadcrumbs: [])
13
+ <<~HTML
14
+ <!doctype html>
15
+ <html lang="en">
16
+ <head>
17
+ <meta charset="utf-8">
18
+ <meta name="viewport" content="width=device-width, initial-scale=1">
19
+ <title>#{Escape.html(title)} · hecks forms</title>
20
+ <style>#{STYLE}</style>
21
+ </head>
22
+ <body>
23
+ <header class="site-header">
24
+ <a class="brand" href="/">hecks forms</a>
25
+ #{breadcrumbs_html(breadcrumbs)}
26
+ </header>
27
+ <main>
28
+ #{body}
29
+ </main>
30
+ <footer class="site-footer">
31
+ <p>Generated straight from the loaded bluebook IR — nothing here is hand-authored per page. See <code>docs/command-form-and-query-form-bluebook.md</code>.</p>
32
+ </footer>
33
+ <script>#{SCRIPT}</script>
34
+ </body>
35
+ </html>
36
+ HTML
37
+ end
38
+
39
+ def self.breadcrumbs_html(crumbs)
40
+ return "" if crumbs.empty?
41
+
42
+ items = crumbs.map do |label, href|
43
+ href ? %(<a href="#{Escape.attr(href)}">#{Escape.html(label)}</a>) : %(<span aria-current="page">#{Escape.html(label)}</span>)
44
+ end
45
+ %(<nav class="breadcrumbs" aria-label="Breadcrumb">#{items.join(' <span class="sep">/</span> ')}</nav>)
46
+ end
47
+
48
+ STYLE = <<~CSS
49
+ :root {
50
+ --bg: #fbfaf8; --surface: #ffffff; --border: #e2ddd3; --text: #24211c;
51
+ --muted: #6b6355; --accent: #8a5a2b; --accent-contrast: #fff8ef;
52
+ --danger-bg: #fdeceb; --danger-border: #e2a49c; --danger-text: #7a2419;
53
+ --focus: #2b6cb0; --code-bg: #f1ede4; --radius: 8px;
54
+ --mono: "SF Mono", "JetBrains Mono", ui-monospace, Menlo, Consolas, monospace;
55
+ --sans: -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
56
+ }
57
+ @media (prefers-color-scheme: dark) {
58
+ :root {
59
+ --bg: #17140f; --surface: #201c15; --border: #3a3327; --text: #ece6d9;
60
+ --muted: #a89d87; --accent: #d99a52; --accent-contrast: #241a0c;
61
+ --danger-bg: #341613; --danger-border: #7a352b; --danger-text: #f3a99c;
62
+ --focus: #6ab0ff; --code-bg: #241f17;
63
+ }
64
+ }
65
+ * { box-sizing: border-box; }
66
+ body { background: var(--bg); color: var(--text); font-family: var(--sans); margin: 0; line-height: 1.5; }
67
+ a { color: var(--accent); }
68
+ a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible {
69
+ outline: 2px solid var(--focus); outline-offset: 2px;
70
+ }
71
+ .site-header { display: flex; align-items: baseline; gap: 1rem; padding: .9rem 1.5rem;
72
+ border-bottom: 1px solid var(--border); background: var(--surface); flex-wrap: wrap; }
73
+ .brand { font-family: var(--mono); font-weight: 600; text-decoration: none; color: var(--text); }
74
+ .breadcrumbs { font-size: .9rem; color: var(--muted); }
75
+ .breadcrumbs .sep { padding: 0 .3rem; }
76
+ main { max-width: 46rem; margin: 0 auto; padding: 2rem 1.5rem 4rem; }
77
+ .site-footer { max-width: 46rem; margin: 0 auto; padding: 1rem 1.5rem 3rem; color: var(--muted); font-size: .82rem; }
78
+ h1 { font-size: 1.5rem; margin: 0 0 .25rem; }
79
+ h2 { font-size: 1.1rem; margin: 2rem 0 .6rem; }
80
+ .goal { color: var(--muted); margin: 0 0 1rem; }
81
+ .badge { display: inline-block; font-family: var(--mono); font-size: .72rem; padding: .15rem .5rem;
82
+ border-radius: 999px; border: 1px solid var(--border); color: var(--muted); margin: 0 .3rem .3rem 0; }
83
+ .badge.role { color: var(--accent); border-color: var(--accent); }
84
+ .callout { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
85
+ padding: .8rem 1rem; margin: 0 0 1.2rem; font-size: .92rem; }
86
+ .callout ul { margin: .3rem 0 0; padding-left: 1.2rem; }
87
+ .error-banner { background: var(--danger-bg); border: 1px solid var(--danger-border); color: var(--danger-text);
88
+ border-radius: var(--radius); padding: .8rem 1rem; margin: 0 0 1.2rem; }
89
+ .error-banner p { margin: 0; }
90
+ fieldset { border: 1px solid var(--border); border-radius: var(--radius); margin: 0 0 1.1rem; padding: .8rem 1rem 1rem; }
91
+ legend { font-size: .85rem; color: var(--muted); padding: 0 .4rem; }
92
+ .field { margin: 0 0 1.1rem; }
93
+ .field > label { display: block; font-weight: 600; font-size: .9rem; margin-bottom: .3rem; }
94
+ .field .required-mark { color: var(--accent); }
95
+ .field .help { display: block; color: var(--muted); font-size: .8rem; margin-top: .3rem; }
96
+ .field input[type=text], .field input[type=email], .field input[type=url], .field input[type=tel],
97
+ .field input[type=number], .field select, .field textarea {
98
+ width: 100%; padding: .5rem .6rem; border: 1px solid var(--border); border-radius: var(--radius);
99
+ background: var(--surface); color: var(--text); font: inherit;
100
+ }
101
+ .field textarea { min-height: 4.5rem; font-family: var(--mono); font-size: .85rem; }
102
+ .radio-group { display: flex; flex-wrap: wrap; gap: .3rem 1rem; }
103
+ .radio-group label { font-weight: 400; display: flex; align-items: center; gap: .35rem; }
104
+ .checkbox-row { display: flex; align-items: center; gap: .5rem; }
105
+ button, .button { font: inherit; border-radius: var(--radius); border: 1px solid var(--accent);
106
+ background: var(--accent); color: var(--accent-contrast); padding: .55rem 1.1rem; cursor: pointer; }
107
+ button.secondary, .button.secondary { background: transparent; color: var(--text); border-color: var(--border); }
108
+ button.copy { font-size: .75rem; padding: .2rem .5rem; }
109
+ table { width: 100%; border-collapse: collapse; font-size: .88rem; }
110
+ .table-scroll { overflow-x: auto; margin: 0 0 1.2rem; }
111
+ th, td { text-align: left; padding: .45rem .6rem; border-bottom: 1px solid var(--border); white-space: nowrap; }
112
+ thead th { color: var(--muted); font-weight: 600; position: sticky; top: 0; background: var(--bg); }
113
+ tbody tr:hover { background: var(--surface); }
114
+ code, pre, .mono { font-family: var(--mono); font-size: .82rem; }
115
+ pre { background: var(--code-bg); border-radius: var(--radius); padding: .8rem 1rem; overflow-x: auto; margin: 0; }
116
+ .link-row { display: flex; gap: .5rem; align-items: center; margin: 0 0 .8rem; }
117
+ .link-row code { flex: 1; overflow-x: auto; padding: .4rem .6rem; background: var(--code-bg); border-radius: var(--radius); }
118
+ .example-links { display: flex; flex-wrap: wrap; gap: .4rem; margin: 0 0 1rem; }
119
+ .example-links a { border: 1px solid var(--border); border-radius: 999px; padding: .25rem .7rem;
120
+ text-decoration: none; font-family: var(--mono); font-size: .82rem; }
121
+ details.inspect { margin: 2rem 0 0; border-top: 1px dashed var(--border); padding-top: 1rem; }
122
+ details.inspect summary { cursor: pointer; color: var(--muted); font-size: .85rem; }
123
+ .verb-list { list-style: none; padding: 0; margin: 0; }
124
+ .verb-list li { border-bottom: 1px solid var(--border); }
125
+ .verb-list a { display: flex; justify-content: space-between; gap: 1rem; padding: .7rem .2rem;
126
+ text-decoration: none; color: var(--text); }
127
+ .verb-list .kind { color: var(--muted); font-family: var(--mono); font-size: .78rem; }
128
+ .actions { display: flex; flex-wrap: wrap; gap: .5rem; margin: .6rem 0 1.4rem; }
129
+ CSS
130
+
131
+ # Decorative only — every form here already works with JS disabled
132
+ # (plain `<form method=post>`/`method=get`), matching this repo's own
133
+ # "no ERB, nothing scripted" bar for the domain layer itself.
134
+ SCRIPT = <<~JS
135
+ document.addEventListener("click", (event) => {
136
+ const button = event.target.closest("[data-copy]");
137
+ if (!button) return;
138
+ const text = document.querySelector(button.getAttribute("data-copy"))?.textContent ?? "";
139
+ navigator.clipboard?.writeText(text.trim());
140
+ const original = button.textContent;
141
+ button.textContent = "copied";
142
+ setTimeout(() => { button.textContent = original; }, 1200);
143
+ });
144
+ document.querySelectorAll("[data-money-cents]").forEach((input) => {
145
+ const preview = document.querySelector(input.getAttribute("data-money-cents"));
146
+ if (!preview) return;
147
+ const update = () => {
148
+ const cents = parseInt(input.value, 10);
149
+ preview.textContent = Number.isFinite(cents) ? "= " + (cents / 100).toFixed(2) : "";
150
+ };
151
+ input.addEventListener("input", update);
152
+ update();
153
+ });
154
+ JS
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,132 @@
1
+ require "json"
2
+ require "uri"
3
+ require_relative "field_renderer"
4
+
5
+ module Hecks
6
+ module Forms
7
+ # The two directions between a flat, dotted, ALL-STRINGS web payload
8
+ # (`{"amount.cents"=>"1050", "amount.currency"=>"USD"}`, whether it came
9
+ # off a POST form body or a GET query string — Rack hands back the same
10
+ # flat shape for both as long as nothing uses `[]` bracket names) and the
11
+ # nested, typed hash `Dispatcher#dispatch`/`#query` actually take
12
+ # (`{amount: {cents: 1050, currency: "USD"}}`).
13
+ #
14
+ # `extract` NEEDS the Field tree, not just the raw params — a numeric
15
+ # leaf's own runtime check (`Value::Coercion#check_numeric_fields`)
16
+ # requires an actual `Integer`/`Float`, not a String that merely looks
17
+ # like one (`given.is_a?(expected)`, no coercion attempted there); a web
18
+ # form can only ever hand back strings, so the cast has to happen here,
19
+ # once, using the SAME shape `FieldShape` already resolved for
20
+ # rendering the input in the first place — one reading of the IR, not
21
+ # two that could disagree.
22
+ module Params
23
+ # Every LEAF field carries its own full dotted path regardless of how
24
+ # deep `FieldShape` nested it to get there — a single-attribute value
25
+ # object unwraps to a leaf sitting at the TOP of the fields array with
26
+ # a two-segment path (`"reference.value"`), the exact same shape a
27
+ # `:group`'s own child carries. So this collects every leaf as
28
+ # (full path -> value) FIRST, and nests by the path's OWN segments
29
+ # last — one nesting rule, blind to how a field arrived at its path.
30
+ def self.extract(fields, raw)
31
+ pairs = {}
32
+ fields.each { |field| collect(field, raw, pairs) }
33
+ nest(pairs)
34
+ end
35
+
36
+ def self.collect(field, raw, pairs)
37
+ case field.kind
38
+ when :group, :money then field.children.each { |child| collect(child, raw, pairs) }
39
+ when :list then (value = extract_list(field, raw)) == SKIP || pairs[field.path] = value
40
+ else (value = extract_leaf(field, raw)) == SKIP || pairs[field.path] = value
41
+ end
42
+ end
43
+
44
+ def self.nest(pairs)
45
+ pairs.each_with_object({}) do |(path, value), result|
46
+ segments = path.to_s.split(".").map(&:to_sym)
47
+ leaf = segments.pop
48
+ node = segments.reduce(result) { |acc, segment| acc[segment] ||= {} }
49
+ node[leaf] = value
50
+ end
51
+ end
52
+
53
+ SKIP = Object.new.freeze
54
+ private_constant :SKIP
55
+
56
+ # One line of the textarea per element. A line that itself needs
57
+ # several fields (a multi-attribute value object as a list element)
58
+ # is read as JSON on that one line — the honest fallback documented in
59
+ # docs/command-form-and-query-form-bluebook.md rather than a second
60
+ # widget this prototype doesn't build yet.
61
+ def self.extract_list(field, raw)
62
+ text = raw[field.path]
63
+ return SKIP if text.nil? || text.strip.empty?
64
+
65
+ item = field.children.first
66
+ text.each_line.map(&:strip).reject(&:empty?).map do |line|
67
+ item.leaf? ? cast_scalar(item, line) : JSON.parse(line, symbolize_names: true)
68
+ end
69
+ end
70
+
71
+ def self.extract_leaf(field, raw)
72
+ return checkbox(raw[field.path]) if field.kind == :boolean
73
+
74
+ text = raw[field.path]
75
+ return SKIP if (text.nil? || text.empty?) && field.optional?
76
+
77
+ cast_scalar(field, text)
78
+ end
79
+
80
+ def self.checkbox(raw_value)
81
+ %w[on 1 true].include?(raw_value.to_s.downcase)
82
+ end
83
+
84
+ def self.cast_scalar(field, text)
85
+ case field.kind
86
+ when :number then field.step == "1" ? Integer(text) : Float(text)
87
+ else text
88
+ end
89
+ end
90
+
91
+ # The other direction — a nested value hash back to the flat dotted
92
+ # pairs a GET link's query string carries, so a query view's
93
+ # "shareable link" and its filter FORM stay two renderings of the
94
+ # SAME data rather than two formats that can drift. Reads with
95
+ # `FieldRenderer.dig` — the identical full-path lookup a re-rendered
96
+ # input's own value comes from — for the same reason `extract` above
97
+ # nests by full path rather than by tree shape.
98
+ def self.flatten(fields, values, into: {})
99
+ fields.each do |field|
100
+ case field.kind
101
+ when :group, :money then flatten(field.children, values, into: into)
102
+ when :list then into[field.path] = Array(FieldRenderer.dig(values, field.path)).join("\n")
103
+ else
104
+ value = FieldRenderer.dig(values, field.path)
105
+ into[field.path] = value.to_s unless value.nil?
106
+ end
107
+ end
108
+ into
109
+ end
110
+
111
+ # Every leaf/list path a field tree carries, independent of any
112
+ # values — what `command_form_renderer.rb`'s inspect panel wants
113
+ # ("which fields does this command take"), where `flatten` above wants
114
+ # "what does THIS submission look like" and returns nothing for a
115
+ # field nothing was entered for.
116
+ def self.paths(fields, into: [])
117
+ fields.each do |field|
118
+ case field.kind
119
+ when :group, :money then paths(field.children, into: into)
120
+ else into << field.path
121
+ end
122
+ end
123
+ into
124
+ end
125
+
126
+ def self.to_query_string(fields, values)
127
+ pairs = flatten(fields, values)
128
+ pairs.map { |key, value| "#{URI.encode_www_form_component(key)}=#{URI.encode_www_form_component(value)}" }.join("&")
129
+ end
130
+ end
131
+ end
132
+ end