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,500 @@
1
+ require_relative "../../bluebook/expression/evaluator"
2
+ require_relative "../../naming"
3
+ require_relative "../../rendering"
4
+ require_relative "../errors"
5
+ require_relative "../refusal_wording"
6
+ require_relative "invariant_violation"
7
+
8
+ module Hecks
9
+ module Runtime
10
+ class Value
11
+ # The class-side engine: how a raw argument or stored field becomes a
12
+ # typed Value. Extended into Value, so every method here reads as
13
+ # `Value.for`, `Value.build`, … — `self` is the Value class.
14
+ module Coercion
15
+ # THE FOUR SHAPES AN ATTRIBUTE'S VALUE CAN TAKE — named here because
16
+ # `for_attribute` immediately below is the one place that actually
17
+ # branches on all four, and nowhere else in the language collects
18
+ # them into a single closed list. `Attribute#list?`/`#optional?`
19
+ # are real predicates on the IR node itself (bluebook/attribute.rb);
20
+ # `:scalar` and `:composite` are not named predicates there — they
21
+ # fall out of whether `aggregate.value_object(attribute.type)`
22
+ # resolves to something, read directly in the `coerced =` line
23
+ # below — but the branch is exactly as real, so it gets a name here
24
+ # too rather than staying anonymous.
25
+ #
26
+ # A SECOND RUNTIME'S KERNEL PORTS THIS METHOD BY HAND (rust/src/
27
+ # kernel/attribute_shapes/*.rs — one file per name in this array,
28
+ # generated into a Rust enum by bin/project_kernel_capabilities so
29
+ # every match over it is compiler-checked exhaustive). If a fifth
30
+ # branch is ever added to `for_attribute`, add its name here in the
31
+ # same breath — this list is that port's only source of truth for
32
+ # "which shapes exist," and a shape missing from it is a shape the
33
+ # generated Rust enum, and therefore the kernel, can never learn
34
+ # about no matter how correct the Ruby below is.
35
+ SHAPES = %i[scalar list optional composite].freeze
36
+
37
+ def for(aggregate, name, value)
38
+ attribute = aggregate.attribute(name)
39
+ return value unless attribute
40
+
41
+ for_attribute(aggregate, attribute, value)
42
+ end
43
+
44
+ # The four `SHAPES` above, in the order this method actually checks
45
+ # them: `optional`/nil-passthrough first (a value that isn't there
46
+ # has no shape left to branch on), `list` second (a list of
47
+ # elements, hydrated as entities), then — inside `coerced =` —
48
+ # `composite` (the type names a declared value object, rebuilt
49
+ # recursively via `build`) with `scalar` as what's left once
50
+ # neither of those applies (the raw value, passed through
51
+ # unchanged).
52
+ def for_attribute(aggregate, attribute, value)
53
+ return value if attribute.nil? || value.nil? # :optional
54
+ return reference_list(attribute, value) if attribute.list? && attribute.reference?
55
+ return reference_identity(attribute, value) if attribute.reference?
56
+ return hydrate_entity_list(aggregate, attribute, value) if attribute.list? # :list
57
+ return value unless aggregate.respond_to?(:value_object)
58
+
59
+ # THE SET THE ATTRIBUTE NAMES IS CHECKED WHERE THE ATTRIBUTE IS KNOWN.
60
+ # `build` below sees only the value object, never which attribute asked
61
+ # for it, so a command argument's `admits:` has to be read here — this
62
+ # is the door every argument and every head field comes through.
63
+ #
64
+ # AFTER coercion, not before: a scalar arrives wrapped in whatever holder
65
+ # its type names (`{value: "append"}` for an OpName), and checking the
66
+ # raw payload would be checking the envelope.
67
+ coerced = value_object_for(aggregate, attribute.type)
68
+ .then do |value_object|
69
+ if value.is_a?(self) && value.type_name == value_object&.hecks_name
70
+ value
71
+ elsif value_object
72
+ build(value_object, fields_for(value_object, attribute.name, value), aggregate)
73
+ else
74
+ value
75
+ end
76
+ end
77
+
78
+ admit_declared_set(aggregate, attribute, coerced)
79
+ coerced
80
+ end
81
+
82
+ # Aggregate-local value objects remain authoritative, which permits
83
+ # intentional duplication. An ordinary fact may also name an identity
84
+ # value object declared on another aggregate; that shape is borrowed
85
+ # only when every chapter declaration with the name agrees.
86
+ def value_object_for(aggregate, type)
87
+ local = aggregate.value_object(type)
88
+ return local if local
89
+
90
+ chapter = aggregate.respond_to?(:hecks_owner) ? aggregate.hecks_owner : nil
91
+ return nil unless chapter.respond_to?(:aggregates)
92
+
93
+ matches = chapter.aggregates.filter_map { |candidate| candidate.value_object(type) }
94
+ shapes = matches.group_by do |shape|
95
+ shape.attributes.map { |field| [field.name, field.type.to_s, field.list?, field.optional?] }
96
+ end
97
+ shapes.size == 1 ? matches.first : nil
98
+ end
99
+
100
+ # Retained relationships store canonical target identities, not Ruby
101
+ # Value wrappers. Raw scalar IDs remain a compatibility input. A named
102
+ # identity VO omits its minted aggregate field at the command boundary;
103
+ # a bespoke compound VO may instead name the target heads directly.
104
+ # Neither form requires reverse-splitting a canonical ID.
105
+ def reference_identity(attribute, value)
106
+ return value unless value.is_a?(self) || value.is_a?(Hash)
107
+
108
+ target = attribute.type.resolve
109
+ return value unless target
110
+
111
+ materialized = materialize(value)
112
+ paths = target.identity_paths
113
+ return value if paths.empty?
114
+
115
+ direct_head = if value.is_a?(self) && target.identity_heads.one?
116
+ head = target.identity_heads.first
117
+ target.attribute(head)&.type.to_s == value.type_name ? head.to_s : nil
118
+ end
119
+
120
+ parts = paths.map do |path|
121
+ segments = path.to_s.split(".")
122
+ segments.shift if direct_head && segments.first == direct_head
123
+ segments.reduce(materialized) do |held, segment|
124
+ held.is_a?(Hash) ? (held[segment.to_sym] || held[segment]) : nil
125
+ end
126
+ end
127
+ return value if parts.any? { |part| part.nil? || (part.respond_to?(:empty?) && part.empty?) }
128
+
129
+ Naming.identity(parts)
130
+ end
131
+
132
+ def reference_list(attribute, value)
133
+ unless value.is_a?(Array)
134
+ raise TypeMismatch,
135
+ "#{attribute.name} is a has_many relationship — pass a list of identities"
136
+ end
137
+
138
+ Freezer.deep(value.dup)
139
+ end
140
+
141
+ def fields_for(value_object, name, value)
142
+ return value.transform_keys(&:to_sym) if value.is_a?(Hash)
143
+ # Mutations may legitimately carry a value object into a differently
144
+ # named value-object slot with the same declared fields (for example,
145
+ # PositiveMoney into an Account's Money balance). Rebuild the target
146
+ # type from its state; callers at the public boundary still have to
147
+ # supply an object rather than a scalar.
148
+ return value.to_h if value.is_a?(self)
149
+
150
+ # Vendored addition, not (yet) upstream hecks (migration
151
+ # plan task 5): a bare scalar auto-wraps into a single-field
152
+ # value object's sole attribute -- the SAME shape
153
+ # #from_identifier already establishes for identity coercion
154
+ # (`build(value_object, { fields.first.name => identifier }) if
155
+ # fields.size == 1`), made consistent here for MUTATION
156
+ # coercion too. Real, corpus-wide gap: a synthesised single-
157
+ # field wrapper (Part 3a's bare-primitive auto-synthesis, the
158
+ # norm for a VO-typed aggregate field) is exactly the shape
159
+ # #rewrap_arithmetic_result hands back a raw scalar RESULT to
160
+ # -- without this, every phantom-field increment/multiply on a
161
+ # single-field-wrapped attribute refused with "pass its fields
162
+ # as an object, not <scalar>" the instant it tried to re-wrap
163
+ # its own correctly-computed result. Multi-field VOs still
164
+ # refuse below, unchanged -- only the genuinely unambiguous
165
+ # single-field case auto-wraps, matching from_identifier's own
166
+ # precedent exactly.
167
+ if value_object.attributes.size == 1
168
+ return { value_object.attributes.first.name => value }
169
+ end
170
+
171
+ raise TypeMismatch,
172
+ RefusalWording.render("TypeMismatch", "value_object_shape",
173
+ name: name, type: value_object.hecks_name,
174
+ offered: Rendering.describe(value))
175
+ end
176
+
177
+ # `build`'s own recursive twin of `for_attribute`'s single-level
178
+ # normalization — a value object's OWN composite-typed fields
179
+ # (`Pizza.price_cents`, a `Price`) never otherwise pass back
180
+ # through `fields_for`, so a bare scalar or partial Hash for one
181
+ # of THOSE sails past the outer VO's own shape check (`Pizza`
182
+ # itself has two fields, so nothing unwraps there) and lands
183
+ # stored one field down exactly as handed in — found live: once
184
+ # the fuzzer actually generated the bare-scalar shape
185
+ # `fields_for` has accepted at the TOP level since 86727afd, a
186
+ # nested `Price` stored as a raw Integer broke every later
187
+ # dotted-path read (`pizza.price_cents.cents`) expecting one
188
+ # more level of Hash.
189
+ #
190
+ # Stays a plain Hash, never a nested `Value` — `Value#with`'s own
191
+ # header and `materialize_unwrapped`'s comment already depend on
192
+ # a value-object-typed field of ANOTHER value object staying a
193
+ # plain Hash once stored, and this does not change that; it only
194
+ # makes sure that Hash has the shape its own type declares.
195
+ # `aggregate` is the one thing `build` didn't used to need — a
196
+ # nested type can only be resolved through `aggregate.
197
+ # value_object(name)`, so callers with no aggregate in reach
198
+ # (`Value#with`, always re-setting an already-scalar arithmetic
199
+ # field) simply skip this and keep their prior behavior.
200
+ def normalize_composite_fields(aggregate, value_object, fields)
201
+ return fields unless aggregate&.respond_to?(:value_object)
202
+
203
+ value_object.attributes.each do |attribute|
204
+ next if attribute.list? || !fields.key?(attribute.name)
205
+
206
+ raw = fields[attribute.name]
207
+ next if raw.nil? || raw.is_a?(self)
208
+
209
+ nested = value_object_for(aggregate, attribute.type)
210
+ next unless nested
211
+
212
+ fields[attribute.name] = normalize_composite_fields(aggregate, nested, fields_for(nested, attribute.name, raw))
213
+ end
214
+
215
+ fields
216
+ end
217
+
218
+ def build(value_object, fields, aggregate = nil)
219
+ fields = value_object.attributes.each_with_object(fields.transform_keys(&:to_sym)) do |attribute, completed|
220
+ completed[attribute.name] = attribute.default unless completed.key?(attribute.name) || attribute.default.nil?
221
+ end
222
+ fields = normalize_composite_fields(aggregate, value_object, fields)
223
+ admit_member(value_object, fields)
224
+ check_admitted(value_object, fields)
225
+ check_numeric_fields(value_object, fields)
226
+ check_scalar_shapes(value_object, fields)
227
+ check_patterns(value_object, fields)
228
+ value_object.invariants.each do |invariant|
229
+ next if Bluebook::Expression::Evaluator.call(invariant.canonical, fields)
230
+
231
+ raise InvariantViolation,
232
+ RefusalWording.render("InvariantViolation", "value_object_invariant",
233
+ name: value_object.hecks_name, description: invariant.description,
234
+ offered: canonical_fields(fields))
235
+ end
236
+ new(value_object, fields)
237
+ end
238
+
239
+ def hydrate(aggregate, state)
240
+ state.each_with_object({}) do |(name, value), hydrated|
241
+ key = name.to_sym
242
+ attribute = aggregate.attribute(key)
243
+ hydrated[key] = attribute ? for_attribute(aggregate, attribute, value) : value
244
+ end
245
+ end
246
+
247
+ # S17, ADR 0026 — SEARCHES THE WHOLE ENTITY TREE, not only the
248
+ # root's own direct children. `aggregate` here is always the
249
+ # ROOT aggregate — `for_attribute`'s own `aggregate` argument is
250
+ # never reassigned as hydration recurses into a nested element,
251
+ # because coercion has to resolve value objects, and only the
252
+ # root answers `.value_object` at all (Entity's own header
253
+ # comment: an entity must NOT answer to it, or `Value.
254
+ # for_attribute` could no longer tell a piece from a head). So
255
+ # a NESTED entity — Dispatch, inside Handler — is not a direct
256
+ # child of the root the way Handler itself is, and a plain
257
+ # `aggregate.entities.find` stops one level short of it.
258
+ def find_entity(construct, name)
259
+ construct.entities.each do |candidate|
260
+ return candidate if candidate.hecks_name == name
261
+
262
+ found = find_entity(candidate, name)
263
+ return found if found
264
+ end
265
+ nil
266
+ end
267
+
268
+ # Frozen through: a list read back out of the store is an answer,
269
+ # not a handle on what is stored.
270
+ def hydrate_entity_list(aggregate, attribute, value)
271
+ entity = find_entity(aggregate, attribute.type.to_s)
272
+ return value unless entity
273
+
274
+ Array(value).map do |element|
275
+ next element unless element.is_a?(Hash)
276
+
277
+ element.each_with_object({}) do |(name, field_value), hydrated|
278
+ key = name.to_sym
279
+ field = entity.attribute(key)
280
+ hydrated[key] = field ? for_attribute(aggregate, field, field_value) : field_value
281
+ end
282
+ end
283
+ .then { |hydrated| Freezer.deep(hydrated) }
284
+ end
285
+
286
+ # `Value.identifier` used to live here: hand it a one-field value object
287
+ # and it opened it, so `identified_by :number` could pass for an identity
288
+ # and the runtime would guess which field was meant. THAT GUESS IS GONE.
289
+ # An identity names its field — `identified_by :number` — and the
290
+ # path is what reaches the scalar. A declaration that names no field is
291
+ # refused when the bluebook loads, so nothing has to be unwrapped later.
292
+ #
293
+ # `scalar` below is a different job and stays: rendering a value object
294
+ # into a column or a message, where there is no path to consult.
295
+
296
+ # `Value.reference_id` lived here, opening a reference to find the id
297
+ # inside it. A reference IS the id now — refused at the payload gate if it
298
+ # arrives as anything else — so there is nothing left to open. The comment
299
+ # it carried said retiring it meant changing how references are STORED ;
300
+ # that is what happened.
301
+
302
+ # A REFERENCE IS AN ID, SO AN OBJECT IS NOT ONE.
303
+ #
304
+ # Nothing coerces a reference — `for_attribute` misses on
305
+ # "Reference<Account>", which is no value object's name, and hands the
306
+ # argument straight through. That is why the wrapped form went in
307
+ # unnoticed for as long as it did: there was no place it could be
308
+ # refused, so whatever the first caller wrote became the shape.
309
+ #
310
+ # This is that place. It sits at the payload gate rather than inside
311
+ # coercion because the sentence names the COMMAND, and `for_attribute`
312
+ # never learns which command it is serving.
313
+ #
314
+ # An Array is deliberately not refused here. A reference is never a list
315
+ # today, and inventing a rule for a shape the language cannot declare is
316
+ # how decoration gets written.
317
+ def refuse_object_reference(command, attribute, value)
318
+ return unless attribute.reference?
319
+
320
+ offered = attribute.list? ? Array(value).find { |item| item.is_a?(Hash) || item.is_a?(self) } : value
321
+ return unless offered.is_a?(Hash) || offered.is_a?(self)
322
+
323
+ raise TypeMismatch,
324
+ RefusalWording.render("TypeMismatch", "reference_as_object",
325
+ command: command.hecks_name, attribute: attribute.name,
326
+ known_by: known_by(attribute))
327
+ end
328
+
329
+ # "(Account is known by number)" — what to send instead. No article, on
330
+ # purpose: "an Account" and "a Customer" differ by the target's first
331
+ # letter, and a refusal pinned byte-for-byte should not hinge on an
332
+ # article-choosing rule. Silent when the target is another chapter's,
333
+ # where this runtime cannot see what it is known by.
334
+ #
335
+ # EVERY HEAD, because a caller has to pass every one. This read
336
+ # `identified_by`, which is the SINGLE head and is nil the moment an
337
+ # identity has two parts — so a composite target fell through the guard
338
+ # and the refusal went silent exactly where it had the most to say. A
339
+ # single-path target reads as it always did.
340
+ def known_by(attribute)
341
+ heads = Array(attribute.type.resolve&.identity_heads)
342
+ return "" if heads.empty?
343
+
344
+ " (#{attribute.type.target_name} is known by #{heads.join(', ')})"
345
+ end
346
+
347
+ def scalar(value)
348
+ return value unless value.is_a?(self)
349
+
350
+ fields = value.to_h
351
+ return fields.values.first if fields.size == 1
352
+
353
+ raise TypeMismatch, RefusalWording.render("TypeMismatch", "multi_field_scalar", type: value.type_name)
354
+ end
355
+
356
+ def from_identifier(aggregate, attribute, identifier)
357
+ value_object = value_object_for(aggregate, attribute.type)
358
+ return identifier unless value_object
359
+
360
+ fields = value_object.attributes
361
+ if fields.size == 1
362
+ field = fields.first
363
+ return build(value_object, { field.name => coerce_identifier(field, identifier) })
364
+ end
365
+
366
+ raise TypeMismatch, RefusalWording.render("TypeMismatch", "composite_identity", type: value_object.hecks_name)
367
+ end
368
+
369
+ # Vendored fix, not (yet) upstream hecks (migration plan
370
+ # task 9): `identifier` here is always the DERIVED IDENTITY
371
+ # STRING -- `Identity.of`/`Identity.from` intentionally return
372
+ # one (correct for naming a repository key), and
373
+ # `Runtime::Instance#materialize_identity!` calls `from_identifier`
374
+ # with exactly that string on every fresh hydration -- but when
375
+ # the identity field's OWN declared type is Integer/Float (not
376
+ # the overwhelmingly common String), seeding it straight from
377
+ # that string round-trips a correctly-derived identity back in
378
+ # as the WRONG Ruby type -- and #build's own
379
+ # `check_numeric_fields` (added specifically to catch a genuine
380
+ # CALLER mismatch) then refused the runtime's OWN internal
381
+ # identity seed instead, on every dispatch, valid input or not.
382
+ #
383
+ # Reuses THIS SAME FILE's own `NUMERIC` table (declared-type ->
384
+ # expected-Ruby-class, already read by `check_numeric_fields`)
385
+ # to decide WHICH declared types need converting, and
386
+ # Kernel#Integer/#Float to do the converting. A genuinely
387
+ # malformed identifier (should never happen, since an identity
388
+ # is always derived FROM a correctly-typed field in the first
389
+ # place, but this stays defensive rather than assume it) passes
390
+ # back unconverted, and `check_numeric_fields` refuses it
391
+ # exactly as it always has -- preserving its real job of
392
+ # catching a genuine caller mismatch, not just this migration's
393
+ # own runtime-internal one.
394
+ private def coerce_identifier(field, identifier)
395
+ return identifier unless identifier.is_a?(String) && NUMERIC.key?(field.type.to_s)
396
+
397
+ case field.type.to_s
398
+ when "Integer" then Integer(identifier)
399
+ when "Float" then Float(identifier)
400
+ else identifier
401
+ end
402
+ rescue ArgumentError
403
+ identifier
404
+ end
405
+
406
+ def canonical_fields(fields)
407
+ JSON.generate(fields.sort_by { |name, _| name.to_s }.to_h)
408
+ end
409
+
410
+ # A field declared Integer or Float must ARRIVE as one.
411
+ #
412
+ # Without this a String sails into a numeric field and the failure surfaces
413
+ # later, inside a predicate, as `positive? expects a number, got "three"` —
414
+ # an EvaluationError, which is NOT a domain refusal. So the runtime broke
415
+ # where the domain should have said no, and the run contract recorded the
416
+ # crash beside genuine refusals as though the domain had judged it.
417
+ #
418
+ # Checked BEFORE invariants, because an invariant reading a mistyped field
419
+ # is exactly the thing that used to explode.
420
+ NUMERIC = { "Integer" => Integer, "Float" => Numeric }.freeze
421
+ private def check_numeric_fields(value_object, fields)
422
+ value_object.attributes.each do |attribute|
423
+ expected = NUMERIC[attribute.type.to_s]
424
+ next unless expected
425
+
426
+ given = fields[attribute.name]
427
+ next if given.nil? || given.is_a?(expected)
428
+
429
+ raise TypeMismatch,
430
+ RefusalWording.render("TypeMismatch", "numeric_field",
431
+ type: value_object.hecks_name, field: attribute.name,
432
+ expected: attribute.type, offered: Rendering.describe(given))
433
+ end
434
+ end
435
+
436
+ # A field declared `String` (or a boolean) must not arrive as a
437
+ # COMPOSITE — an Array or a Hash (or a nested Value) standing in for
438
+ # what has to be a leaf scalar.
439
+ #
440
+ # Deliberately laxer than `check_numeric_fields` above : it does not
441
+ # enforce the exact Ruby class, only that the shape isn't a collection.
442
+ # `Judge#v` — the language's own self-hosted grammar validation —
443
+ # hands a String-typed field (`Normalise`'s `position`, a `RuleText`)
444
+ # a raw Integer walk-index on purpose, on every boot, and that has
445
+ # always been tolerated ; a full String-vs-Integer check here would
446
+ # refuse the runtime's own bootstrap. But no scalar field, of any
447
+ # declared type, can ever legitimately be handed an Array or a Hash —
448
+ # that shape is always wrong, and always was: `InvalidValueGenerator#
449
+ # array_for_scalar`'s own corruption is deliberately built to be
450
+ # REFUSED (see that file's header), and until this check existed it
451
+ # sailed straight through for a String/boolean field the way it never
452
+ # could for an Integer/Float one (`check_numeric_fields` above already
453
+ # catches an Array offered for those). Found live via bin/fuzz, seed
454
+ # 17 on the fixtures domain : an Array standing in for a single-field
455
+ # identity's declared `String`, `.to_s`'d into a record id downstream.
456
+ COMPOSITE_SHAPES = [Array, ::Hash].freeze
457
+ NON_NUMERIC_SCALARS = %w[String TrueClass FalseClass].freeze
458
+ private def check_scalar_shapes(value_object, fields)
459
+ value_object.attributes.each do |attribute|
460
+ next unless NON_NUMERIC_SCALARS.include?(attribute.type.to_s)
461
+
462
+ given = fields[attribute.name]
463
+ next if given.nil? || COMPOSITE_SHAPES.none? { |shape| given.is_a?(shape) }
464
+
465
+ raise TypeMismatch,
466
+ RefusalWording.render("TypeMismatch", "numeric_field",
467
+ type: value_object.hecks_name, field: attribute.name,
468
+ expected: attribute.type, offered: Rendering.describe(given))
469
+ end
470
+ end
471
+
472
+ # A field declared with a PATTERN must match it.
473
+ #
474
+ # Beside check_numeric_fields and for the same reason : a value that does
475
+ # not look like what it claims to be is the DOMAIN saying no, and it should
476
+ # say so here rather than let the wrong shape travel on and surface as a
477
+ # broken predicate later.
478
+ #
479
+ # Which regexes may be written at all is PatternSubset's job, enforced when
480
+ # the bluebook is declared — so by the time a value arrives here the pattern
481
+ # is already a vetted, unambiguous one, and this is a plain match.
482
+ private def check_patterns(value_object, fields)
483
+ value_object.attributes.each do |attribute|
484
+ pattern = attribute.pattern
485
+ next unless pattern
486
+
487
+ given = fields[attribute.name]
488
+ next if given.nil?
489
+ next if given.is_a?(String) && Regexp.new(pattern).match?(given)
490
+
491
+ raise TypeMismatch,
492
+ RefusalWording.render("TypeMismatch", "pattern_mismatch",
493
+ type: value_object.hecks_name, field: attribute.name,
494
+ pattern: pattern, offered: Rendering.describe(given))
495
+ end
496
+ end
497
+ end
498
+ end
499
+ end
500
+ end
@@ -0,0 +1,5 @@
1
+ module Hecks
2
+ module Runtime
3
+ class InvariantViolation < StandardError; end
4
+ end
5
+ end
@@ -0,0 +1,125 @@
1
+ require "json"
2
+ require_relative "../rendering"
3
+ require_relative "value/invariant_violation"
4
+ require_relative "value/coercion"
5
+ require_relative "value/admission"
6
+
7
+ module Hecks
8
+ module Runtime
9
+ # A typed value object in hand: frozen fields, read by name. How one is
10
+ # MADE — coerced from a raw argument, checked against its declared
11
+ # numeric types, patterns and closed sets — is the class-side engine in
12
+ # value/coercion.rb and value/admission.rb, extended here so the door
13
+ # stays where it always was: `Value.for`, `Value.build`.
14
+ class Value
15
+ extend Coercion
16
+ extend Admission
17
+
18
+ attr_reader :value_object
19
+
20
+ # FROZEN THROUGH, not just on top.
21
+ #
22
+ # `@fields.freeze` alone stops a key being added or removed and
23
+ # nothing else: the String, Array or Hash a field HOLDS stays
24
+ # mutable, so `vo[:value] << "!"` edits a value object in place —
25
+ # demonstrated on a real dispatch before this was written, not
26
+ # supposed. Same shape as the three freezing bugs already fixed
27
+ # (list attributes, the event log, query rows), where the container
28
+ # was frozen and the contents were not.
29
+ #
30
+ # A value object is the one thing in the domain that has no
31
+ # identity to change over — `with` already answers a NEW one rather
32
+ # than mutating — so freezing it through is what it always claimed
33
+ # to be.
34
+ def initialize(value_object, fields)
35
+ @value_object = value_object
36
+ @fields = Freezer.deep(fields.transform_keys(&:to_sym))
37
+ freeze
38
+ end
39
+
40
+ def type_name = @value_object.hecks_name
41
+ def [](field) = @fields[field.to_sym]
42
+ def key?(field) = @fields.key?(field.to_sym)
43
+ def to_h = @fields.transform_values { |value| self.class.materialize(value) }
44
+ def to_json(*) = JSON.generate(to_h)
45
+
46
+ def ==(other)
47
+ other.is_a?(self.class) && other.type_name == type_name && other.to_h == to_h
48
+ end
49
+
50
+ def with(field, value)
51
+ self.class.build(@value_object, @fields.merge(field.to_sym => value))
52
+ end
53
+
54
+ def self.materialize(value)
55
+ case value
56
+ when self then value.to_h
57
+ when Array then value.map { |item| materialize(item) }
58
+ when Hash then value.transform_values { |item| materialize(item) }
59
+ else value
60
+ end
61
+ end
62
+
63
+ # `materialize`, but a single-attribute value object (`sole_attribute`
64
+ # — [[feedback_name_the_scalar_field]]) recurses into its own bare
65
+ # field instead of building `{field: ...}` — a Board's own `label`
66
+ # unwraps to `"Kanban"`, not `{value: "Kanban"}`. NOT a replacement
67
+ # for `materialize` itself: every existing report/query/command
68
+ # caller keeps the wrapped shape it already depends on (Banking's
69
+ # own `CustomerPortfolio` reads `payment[:amount][:cents]`, and
70
+ # changing that out from under it would be a real breaking change,
71
+ # not a bug fix). This is read_model_interpreter.rb's own opt-in,
72
+ # used ONLY for a `group_by`-declared head's own rows — grouping
73
+ # needs a real scalar to key by regardless, so a report already
74
+ # asking for that gets the unwrap for free.
75
+ def self.materialize_unwrapped(value)
76
+ case value
77
+ when self
78
+ sole = value.value_object.sole_attribute
79
+ return materialize_unwrapped(value[sole.name]) if sole
80
+
81
+ # NOT `value.to_h` — `Value#to_h` materializes each field through
82
+ # plain `materialize`, so a VO nested inside a multi-attribute VO
83
+ # would already be a plain Hash by the time this method ever saw
84
+ # it, and never reach the `when self` branch above. Read each
85
+ # field straight off `value` instead, so recursion actually
86
+ # happens through THIS method the whole way down.
87
+ value.value_object.attributes.to_h { |attr| [attr.name, materialize_unwrapped(value[attr.name])] }
88
+ when Array then value.map { |item| materialize_unwrapped(item) }
89
+ when Hash then value.transform_values { |item| materialize_unwrapped(item) }
90
+ else value
91
+ end
92
+ end
93
+
94
+ # REDUCES AN APPEND-ONLY SUB-LOG TO ITS CURRENT STATE — the same
95
+ # "a later fact supersedes an earlier one" reduction this runtime
96
+ # already performs replaying an AGGREGATE's own command history
97
+ # into its current attributes, applied here to a single `list_of`
98
+ # FIELD acting as its own miniature append-only log (a placement
99
+ # history, a tombstone-style soft-delete list, a versioned
100
+ # setting). `rows` is what a `list_of` attribute hands back — an
101
+ # Array of `Value`, in append order — and `key` names the field
102
+ # that identifies "the same logical thing" across entries.
103
+ #
104
+ # GROUPING ONLY, NEVER INTERPRETATION. What counts as "removed,"
105
+ # how to order what survives — that meaning belongs to whichever
106
+ # domain declared the field, never here: a generic reduction that
107
+ # started guessing domain semantics would need to keep guessing
108
+ # forever, once per shape of "gone" any caller ever invents. The
109
+ # caller filters and sorts the result; this only groups it.
110
+ def self.latest_by(rows, key)
111
+ rows.each_with_object({}) { |row, latest| latest[row.public_send(key)] = row }.values
112
+ end
113
+
114
+ def method_missing(name, *args)
115
+ return @fields[name] if @fields.key?(name)
116
+
117
+ super
118
+ end
119
+
120
+ def respond_to_missing?(name, include_private = false)
121
+ @fields.key?(name) || super
122
+ end
123
+ end
124
+ end
125
+ end