hecks 0.2.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (419) hide show
  1. checksums.yaml +5 -5
  2. data/lib/hecks/adapters/driven/claude_code.adapter +3 -0
  3. data/lib/hecks/adapters/driven/claude_code.rb +127 -0
  4. data/lib/hecks/adapters/driven/d1.adapter +12 -0
  5. data/lib/hecks/adapters/driven/d1.rb +427 -0
  6. data/lib/hecks/adapters/driven/folder.adapter +3 -0
  7. data/lib/hecks/adapters/driven/folder.rb +199 -0
  8. data/lib/hecks/adapters/driven/google_authentication.adapter +3 -0
  9. data/lib/hecks/adapters/driven/google_authentication.rb +100 -0
  10. data/lib/hecks/adapters/driven/governance_authorization.adapter +3 -0
  11. data/lib/hecks/adapters/driven/governance_authorization.rb +101 -0
  12. data/lib/hecks/adapters/driven/heki/journal.rb +56 -0
  13. data/lib/hecks/adapters/driven/heki/saga_store.rb +99 -0
  14. data/lib/hecks/adapters/driven/heki/snapshot.rb +65 -0
  15. data/lib/hecks/adapters/driven/heki.adapter +4 -0
  16. data/lib/hecks/adapters/driven/heki.rb +179 -0
  17. data/lib/hecks/adapters/driven/identity_registry.adapter +3 -0
  18. data/lib/hecks/adapters/driven/identity_registry.rb +26 -0
  19. data/lib/hecks/adapters/driven/in_memory_ordering.rb +51 -0
  20. data/lib/hecks/adapters/driven/lambda/client.rb +63 -0
  21. data/lib/hecks/adapters/driven/lambda.adapter +4 -0
  22. data/lib/hecks/adapters/driven/lambda.rb +145 -0
  23. data/lib/hecks/adapters/driven/memory.adapter +3 -0
  24. data/lib/hecks/adapters/driven/memory.rb +103 -0
  25. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +3 -0
  26. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +28 -0
  27. data/lib/hecks/adapters/driven/postgres/codec.rb +88 -0
  28. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +207 -0
  29. data/lib/hecks/adapters/driven/postgres.adapter +5 -0
  30. data/lib/hecks/adapters/driven/postgres.rb +438 -0
  31. data/lib/hecks/adapters/driven/postgres_era.adapter +17 -0
  32. data/lib/hecks/adapters/driven/prism.adapter +3 -0
  33. data/lib/hecks/adapters/driven/prism.rb +80 -0
  34. data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
  35. data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
  36. data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
  37. data/lib/hecks/adapters/driven/sqlite/codec.rb +80 -0
  38. data/lib/hecks/adapters/driven/sqlite/projection.rb +172 -0
  39. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +192 -0
  40. data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
  41. data/lib/hecks/adapters/driven/sqlite.rb +306 -0
  42. data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
  43. data/lib/hecks/adapters/driven/system_clock.rb +14 -0
  44. data/lib/hecks/adapters/driven.rb +54 -0
  45. data/lib/hecks/adapters.rb +6 -0
  46. data/lib/hecks/behaviors/dsl.rb +125 -0
  47. data/lib/hecks/behaviors/expectations.rb +340 -0
  48. data/lib/hecks/behaviors/ir.rb +31 -0
  49. data/lib/hecks/behaviors/rspec.rb +42 -0
  50. data/lib/hecks/behaviors/runner.rb +96 -0
  51. data/lib/hecks/behaviors.rb +25 -0
  52. data/lib/hecks/bluebook/aggregate.rb +108 -0
  53. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
  54. data/lib/hecks/bluebook/assembly/build.rb +48 -0
  55. data/lib/hecks/bluebook/assembly/contract.rb +112 -0
  56. data/lib/hecks/bluebook/assembly/contracts.rb +438 -0
  57. data/lib/hecks/bluebook/assembly/marks.rb +228 -0
  58. data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
  59. data/lib/hecks/bluebook/assembly.rb +91 -0
  60. data/lib/hecks/bluebook/attribute.rb +96 -0
  61. data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
  62. data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
  63. data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
  64. data/lib/hecks/bluebook/behaviour/command.rb +116 -0
  65. data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
  66. data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
  67. data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
  68. data/lib/hecks/bluebook/behaviour/lifecycle.rb +68 -0
  69. data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
  70. data/lib/hecks/bluebook/behaviour/process_manager.rb +51 -0
  71. data/lib/hecks/bluebook/behaviour/query.rb +10 -0
  72. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
  73. data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
  74. data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
  75. data/lib/hecks/bluebook/chapter.rb +78 -0
  76. data/lib/hecks/bluebook/command.rb +124 -0
  77. data/lib/hecks/bluebook/domain_port.rb +102 -0
  78. data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
  79. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +1018 -0
  80. data/lib/hecks/bluebook/dsl/attribute_collector.rb +348 -0
  81. data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
  82. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1087 -0
  83. data/lib/hecks/bluebook/dsl/command_builder.rb +714 -0
  84. data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
  85. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
  86. data/lib/hecks/bluebook/dsl/entity_builder.rb +430 -0
  87. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
  88. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
  89. data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
  90. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
  91. data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
  92. data/lib/hecks/bluebook/dsl/policy_builder.rb +135 -0
  93. data/lib/hecks/bluebook/dsl/port_builder.rb +39 -0
  94. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
  95. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +306 -0
  96. data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
  97. data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
  98. data/lib/hecks/bluebook/dsl/rule_reference.rb +173 -0
  99. data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
  100. data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
  101. data/lib/hecks/bluebook/dsl/word_gate.rb +228 -0
  102. data/lib/hecks/bluebook/dsl/world_builder.rb +117 -0
  103. data/lib/hecks/bluebook/dsl.rb +45 -0
  104. data/lib/hecks/bluebook/entity.rb +103 -0
  105. data/lib/hecks/bluebook/expression/canonical_form.rb +123 -0
  106. data/lib/hecks/bluebook/expression/evaluator.rb +304 -0
  107. data/lib/hecks/bluebook/expression/projection.json +218 -0
  108. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
  109. data/lib/hecks/bluebook/expression/resolver.rb +781 -0
  110. data/lib/hecks/bluebook/expression.rb +13 -0
  111. data/lib/hecks/bluebook/hexagon.rb +60 -0
  112. data/lib/hecks/bluebook/lifecycle.rb +42 -0
  113. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
  114. data/lib/hecks/bluebook/meta_validator/judge.rb +621 -0
  115. data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
  116. data/lib/hecks/bluebook/meta_validator/port_judge.rb +51 -0
  117. data/lib/hecks/bluebook/meta_validator/readings.rb +360 -0
  118. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +351 -0
  119. data/lib/hecks/bluebook/meta_validator/shapes.rb +266 -0
  120. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
  121. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
  122. data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
  123. data/lib/hecks/bluebook/meta_validator.rb +542 -0
  124. data/lib/hecks/bluebook/model_check.rb +445 -0
  125. data/lib/hecks/bluebook/pattern_subset.rb +184 -0
  126. data/lib/hecks/bluebook/policy.rb +41 -0
  127. data/lib/hecks/bluebook/process_manager.rb +128 -0
  128. data/lib/hecks/bluebook/project_discovery.rb +30 -0
  129. data/lib/hecks/bluebook/project_loader.rb +40 -0
  130. data/lib/hecks/bluebook/project_register.rb +82 -0
  131. data/lib/hecks/bluebook/query.rb +61 -0
  132. data/lib/hecks/bluebook/read_model.rb +109 -0
  133. data/lib/hecks/bluebook/reference.rb +74 -0
  134. data/lib/hecks/bluebook/smoke_test.rb +166 -0
  135. data/lib/hecks/bluebook/synthesizer.rb +95 -0
  136. data/lib/hecks/bluebook/translation.rb +92 -0
  137. data/lib/hecks/bluebook/value_object.rb +66 -0
  138. data/lib/hecks/bluebook.rb +74 -0
  139. data/lib/hecks/codemod.rb +342 -0
  140. data/lib/hecks/construct.rb +71 -0
  141. data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
  142. data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
  143. data/lib/hecks/deploy/oidc.json +18 -0
  144. data/lib/hecks/doc/reference.rb +410 -0
  145. data/lib/hecks/embryonaut_bluebook.rb +75 -0
  146. data/lib/hecks/facade/cli_door.rb +119 -0
  147. data/lib/hecks/facade/cli_runner.rb +190 -0
  148. data/lib/hecks/facade/command_request.rb +105 -0
  149. data/lib/hecks/facade/handle.rb +173 -0
  150. data/lib/hecks/facade/json_door.rb +166 -0
  151. data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
  152. data/lib/hecks/facade/surface/chapter.rb +107 -0
  153. data/lib/hecks/facade/surface.rb +48 -0
  154. data/lib/hecks/facade.rb +44 -0
  155. data/lib/hecks/forms/app.rb +341 -0
  156. data/lib/hecks/forms/command_form_renderer.rb +113 -0
  157. data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
  158. data/lib/hecks/forms/field_renderer.rb +177 -0
  159. data/lib/hecks/forms/field_shape.rb +232 -0
  160. data/lib/hecks/forms/html.rb +84 -0
  161. data/lib/hecks/forms/index_renderer.rb +35 -0
  162. data/lib/hecks/forms/page.rb +157 -0
  163. data/lib/hecks/forms/params.rb +161 -0
  164. data/lib/hecks/forms/port_argument.rb +46 -0
  165. data/lib/hecks/forms/query_form_renderer.rb +114 -0
  166. data/lib/hecks/forms/record_renderer.rb +119 -0
  167. data/lib/hecks/forms/record_table.rb +68 -0
  168. data/lib/hecks/forms/reference_options.rb +30 -0
  169. data/lib/hecks/forms/value_object_shape.rb +46 -0
  170. data/lib/hecks/forms.rb +54 -0
  171. data/lib/hecks/fqn.rb +94 -0
  172. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
  173. data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
  174. data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
  175. data/lib/hecks/framework/bluebook/governance.bluebook +145 -0
  176. data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
  177. data/lib/hecks/framework/oidc.json +39 -0
  178. data/lib/hecks/framework.rb +90 -0
  179. data/lib/hecks/freezer.rb +67 -0
  180. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +527 -0
  181. data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
  182. data/lib/hecks/fuzzing/isolated_boot.rb +286 -0
  183. data/lib/hecks/fuzzing/properties.rb +1192 -0
  184. data/lib/hecks/fuzzing/replay.rb +668 -0
  185. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
  186. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +80 -0
  187. data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
  188. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +250 -0
  189. data/lib/hecks/fuzzing/sequence_generator.rb +131 -0
  190. data/lib/hecks/fuzzing/value_generator.rb +200 -0
  191. data/lib/hecks/fuzzing.rb +12 -0
  192. data/lib/hecks/grammar/evolve.rb +324 -0
  193. data/lib/hecks/grammar/expression.bluebook +420 -0
  194. data/lib/hecks/grammar/expression_operators.json +1648 -0
  195. data/lib/hecks/grammar/grammar.hecksagon +20 -0
  196. data/lib/hecks/grammar/oidc.json +69 -0
  197. data/lib/hecks/grammar/translation.bluebook +203 -0
  198. data/lib/hecks/grammar.rb +178 -0
  199. data/lib/hecks/ir.rb +126 -0
  200. data/lib/hecks/language/adapter.bluebook +116 -0
  201. data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
  202. data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
  203. data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
  204. data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
  205. data/lib/hecks/language/bluebook/command.bluebook +471 -0
  206. data/lib/hecks/language/bluebook/entity.bluebook +392 -0
  207. data/lib/hecks/language/bluebook/policy.bluebook +189 -0
  208. data/lib/hecks/language/bluebook/process_manager.bluebook +381 -0
  209. data/lib/hecks/language/bluebook/projection.bluebook +267 -0
  210. data/lib/hecks/language/bluebook/query.bluebook +245 -0
  211. data/lib/hecks/language/bluebook/shape.bluebook +292 -0
  212. data/lib/hecks/language/bluebook/syntax.bluebook +448 -0
  213. data/lib/hecks/language/bluebook/vocabulary.bluebook +379 -0
  214. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
  215. data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
  216. data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
  217. data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
  218. data/lib/hecks/language/oidc.json +333 -0
  219. data/lib/hecks/language/port.bluebook +120 -0
  220. data/lib/hecks/language/translation/translation.bluebook +110 -0
  221. data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
  222. data/lib/hecks/language/world/wiring.bluebook +62 -0
  223. data/lib/hecks/language/world/world.bluebook +84 -0
  224. data/lib/hecks/literal.rb +125 -0
  225. data/lib/hecks/naming.rb +174 -0
  226. data/lib/hecks/ports/access_control.port +9 -0
  227. data/lib/hecks/ports/access_control.rb +62 -0
  228. data/lib/hecks/ports/agent/answers.rb +104 -0
  229. data/lib/hecks/ports/agent.port +8 -0
  230. data/lib/hecks/ports/agent.rb +167 -0
  231. data/lib/hecks/ports/authentication.port +6 -0
  232. data/lib/hecks/ports/authentication.rb +50 -0
  233. data/lib/hecks/ports/authorization.port +7 -0
  234. data/lib/hecks/ports/authorization.rb +62 -0
  235. data/lib/hecks/ports/clock.port +5 -0
  236. data/lib/hecks/ports/clock.rb +62 -0
  237. data/lib/hecks/ports/extraction.port +5 -0
  238. data/lib/hecks/ports/extraction.rb +37 -0
  239. data/lib/hecks/ports/identity_assignment.port +5 -0
  240. data/lib/hecks/ports/identity_assignment.rb +45 -0
  241. data/lib/hecks/ports/identity_generation.port +5 -0
  242. data/lib/hecks/ports/identity_generation.rb +49 -0
  243. data/lib/hecks/ports/identity_resolution.port +5 -0
  244. data/lib/hecks/ports/identity_resolution.rb +40 -0
  245. data/lib/hecks/ports/loading.port +4 -0
  246. data/lib/hecks/ports/loading.rb +13 -0
  247. data/lib/hecks/ports/persistence/append_only.rb +138 -0
  248. data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
  249. data/lib/hecks/ports/persistence/execution.rb +23 -0
  250. data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
  251. data/lib/hecks/ports/persistence/plugin.rb +54 -0
  252. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +169 -0
  253. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +128 -0
  254. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +161 -0
  255. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +61 -0
  256. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +304 -0
  257. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +171 -0
  258. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +190 -0
  259. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +478 -0
  260. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +166 -0
  261. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +314 -0
  262. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +168 -0
  263. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +170 -0
  264. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +134 -0
  265. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +137 -0
  266. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +89 -0
  267. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +85 -0
  268. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
  269. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +132 -0
  270. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +74 -0
  271. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +758 -0
  272. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +120 -0
  273. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +31 -0
  274. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +46 -0
  275. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +102 -0
  276. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +46 -0
  277. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +70 -0
  278. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +72 -0
  279. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +120 -0
  280. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +183 -0
  281. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +41 -0
  282. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +28 -0
  283. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +30 -0
  284. data/lib/hecks/ports/persistence/plugins/era/translation.rb +11 -0
  285. data/lib/hecks/ports/persistence/plugins/era.rb +48 -0
  286. data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
  287. data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
  288. data/lib/hecks/ports/persistence.port +4 -0
  289. data/lib/hecks/ports/persistence.rb +39 -0
  290. data/lib/hecks/ports/projection.port +4 -0
  291. data/lib/hecks/ports/projection.rb +96 -0
  292. data/lib/hecks/ports/query/in_memory.rb +60 -0
  293. data/lib/hecks/ports/query/ordering.rb +41 -0
  294. data/lib/hecks/ports/query.rb +36 -0
  295. data/lib/hecks/ports.rb +27 -0
  296. data/lib/hecks/projections/diagrams.rb +642 -0
  297. data/lib/hecks/projections/ir.rb +18 -0
  298. data/lib/hecks/projections/model/deviations.rb +98 -0
  299. data/lib/hecks/projections/model.rb +145 -0
  300. data/lib/hecks/projections/oidc.rb +110 -0
  301. data/lib/hecks/projections/parser_table.rb +159 -0
  302. data/lib/hecks/projections/reference.rb +38 -0
  303. data/lib/hecks/projections/shape.rb +44 -0
  304. data/lib/hecks/projections/statements.rb +110 -0
  305. data/lib/hecks/projections/vocabulary.rb +100 -0
  306. data/lib/hecks/projections.rb +32 -0
  307. data/lib/hecks/projector/cli_projector.rb +446 -0
  308. data/lib/hecks/projector/docs_projector.rb +321 -0
  309. data/lib/hecks/projector/exporter.rb +158 -0
  310. data/lib/hecks/projector/ir_projector.rb +18 -0
  311. data/lib/hecks/projector/narrate_projector.rb +243 -0
  312. data/lib/hecks/projector/target.rb +97 -0
  313. data/lib/hecks/projector.rb +186 -0
  314. data/lib/hecks/query_ir.rb +411 -0
  315. data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
  316. data/lib/hecks/query_specification/common/comparators.rb +25 -0
  317. data/lib/hecks/query_specification/common/comparison.rb +174 -0
  318. data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
  319. data/lib/hecks/query_specification/common/dsl.rb +58 -0
  320. data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
  321. data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
  322. data/lib/hecks/query_specification/common/null_policy.rb +92 -0
  323. data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
  324. data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
  325. data/lib/hecks/query_specification/common/options.rb +32 -0
  326. data/lib/hecks/query_specification/common/order_by.rb +9 -0
  327. data/lib/hecks/query_specification/common/specification.rb +10 -0
  328. data/lib/hecks/query_specification/common/where_clause.rb +9 -0
  329. data/lib/hecks/query_specification/field_path.rb +107 -0
  330. data/lib/hecks/query_specification/hop_path.rb +132 -0
  331. data/lib/hecks/query_specification/read_model/specification.rb +18 -0
  332. data/lib/hecks/query_specification.rb +14 -0
  333. data/lib/hecks/rendering.rb +48 -0
  334. data/lib/hecks/router/namespace_installer.rb +157 -0
  335. data/lib/hecks/router.rb +70 -0
  336. data/lib/hecks/runtime/aggregate_lock.rb +45 -0
  337. data/lib/hecks/runtime/boot_gates.rb +41 -0
  338. data/lib/hecks/runtime/caller.rb +66 -0
  339. data/lib/hecks/runtime/capability_graph.rb +44 -0
  340. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +135 -0
  341. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +260 -0
  342. data/lib/hecks/runtime/command_interpreter.rb +497 -0
  343. data/lib/hecks/runtime/command_rules/admissibility.rb +357 -0
  344. data/lib/hecks/runtime/command_rules/arithmetic.rb +260 -0
  345. data/lib/hecks/runtime/command_rules/authorization.rb +65 -0
  346. data/lib/hecks/runtime/command_rules/emission.rb +34 -0
  347. data/lib/hecks/runtime/command_rules/references.rb +189 -0
  348. data/lib/hecks/runtime/command_rules.rb +28 -0
  349. data/lib/hecks/runtime/dependency_planning.rb +245 -0
  350. data/lib/hecks/runtime/dispatcher.rb +301 -0
  351. data/lib/hecks/runtime/entity_element.rb +253 -0
  352. data/lib/hecks/runtime/entity_interpreter.rb +299 -0
  353. data/lib/hecks/runtime/errors.rb +122 -0
  354. data/lib/hecks/runtime/event.rb +51 -0
  355. data/lib/hecks/runtime/identity.rb +156 -0
  356. data/lib/hecks/runtime/instance.rb +175 -0
  357. data/lib/hecks/runtime/interpreting.rb +90 -0
  358. data/lib/hecks/runtime/loader.rb +184 -0
  359. data/lib/hecks/runtime/policy_interpreter.rb +366 -0
  360. data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
  361. data/lib/hecks/runtime/query_interpreter.rb +286 -0
  362. data/lib/hecks/runtime/reaction_invocation.rb +253 -0
  363. data/lib/hecks/runtime/read_model_interpreter.rb +341 -0
  364. data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
  365. data/lib/hecks/runtime/reference_hop.rb +99 -0
  366. data/lib/hecks/runtime/refusal_wording.rb +126 -0
  367. data/lib/hecks/runtime/registry/saga_persistence.rb +142 -0
  368. data/lib/hecks/runtime/registry/verification.rb +246 -0
  369. data/lib/hecks/runtime/registry.rb +279 -0
  370. data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
  371. data/lib/hecks/runtime/routing.rb +96 -0
  372. data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
  373. data/lib/hecks/runtime/saga_interpreter.rb +503 -0
  374. data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -0
  375. data/lib/hecks/runtime/tenant_check.rb +84 -0
  376. data/lib/hecks/runtime/tenant_scope.rb +55 -0
  377. data/lib/hecks/runtime/value/admission.rb +130 -0
  378. data/lib/hecks/runtime/value/coercion.rb +565 -0
  379. data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
  380. data/lib/hecks/runtime/value.rb +125 -0
  381. data/lib/hecks/runtime.rb +107 -0
  382. data/lib/hecks/storehouse.rb +632 -0
  383. data/lib/hecks/version.rb +16 -0
  384. data/lib/hecks/vocabulary.rb +218 -0
  385. data/lib/hecks.rb +123 -6
  386. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
  387. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
  388. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
  389. metadata +412 -222
  390. data/bin/hecks +0 -7
  391. data/bin/hecks-package +0 -65
  392. data/bin/hecks_console +0 -12
  393. data/bin/hecks_serverless +0 -6
  394. data/lib/cli/build.rb +0 -14
  395. data/lib/cli/command_runner.rb +0 -28
  396. data/lib/cli/console.rb +0 -10
  397. data/lib/cli/generate.rb +0 -37
  398. data/lib/cli/hecks-cli.rb +0 -27
  399. data/lib/cli/test.rb +0 -57
  400. data/lib/console/commands.rb +0 -8
  401. data/lib/console/hecks-console.rb +0 -1
  402. data/lib/packager/README.md +0 -0
  403. data/lib/packager/app_runner.rb +0 -21
  404. data/lib/packager/args.rb +0 -26
  405. data/lib/packager/compatibility/fixnum.rb +0 -6
  406. data/lib/packager/hecks.rb +0 -39
  407. data/lib/packager/query_runner.rb +0 -21
  408. data/lib/packager/resources/Dockerfile +0 -11
  409. data/lib/packager/resources/app_binary +0 -7
  410. data/lib/packager/resources/bundle_config +0 -3
  411. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
  412. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
  413. data/lib/serverless/Domain +0 -32
  414. data/lib/serverless/cli.rb +0 -75
  415. data/lib/serverless/resources/command_name.js +0 -5
  416. data/lib/serverless/resources/environment.js +0 -7
  417. data/lib/serverless/resources/handler.js.tt +0 -28
  418. data/lib/serverless/resources/run_binary.js +0 -22
  419. data/lib/serverless/resources/serverless.yml +0 -20
@@ -0,0 +1,565 @@
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
+ next nil unless held.is_a?(Hash)
125
+
126
+ # `key?` decides which spelling answers, never `||` — a
127
+ # genuinely-held `false` must not fall through to the
128
+ # other spelling (usually absent) and read as `nil`.
129
+ sym = segment.to_sym
130
+ held.key?(sym) ? held[sym] : held[segment]
131
+ end
132
+ end
133
+ return value if parts.any? { |part| part.nil? || (part.respond_to?(:empty?) && part.empty?) }
134
+
135
+ Naming.identity(parts)
136
+ end
137
+
138
+ def reference_list(attribute, value)
139
+ unless value.is_a?(Array)
140
+ raise TypeMismatch,
141
+ "#{attribute.name} is a has_many relationship — pass a list of identities"
142
+ end
143
+
144
+ Freezer.deep(value.dup)
145
+ end
146
+
147
+ def fields_for(value_object, name, value)
148
+ return value.transform_keys(&:to_sym) if value.is_a?(Hash)
149
+ # Mutations may legitimately carry a value object into a differently
150
+ # named value-object slot with the same declared fields (for example,
151
+ # PositiveMoney into an Account's Money balance). Rebuild the target
152
+ # type from its state; callers at the public boundary still have to
153
+ # supply an object rather than a scalar.
154
+ return value.to_h if value.is_a?(self)
155
+
156
+ # Vendored addition, not (yet) upstream hecks (migration
157
+ # plan task 5): a bare scalar auto-wraps into a single-field
158
+ # value object's sole attribute -- the SAME shape
159
+ # #from_identifier already establishes for identity coercion
160
+ # (`build(value_object, { fields.first.name => identifier }) if
161
+ # fields.size == 1`), made consistent here for MUTATION
162
+ # coercion too. Real, corpus-wide gap: a synthesised single-
163
+ # field wrapper (Part 3a's bare-primitive auto-synthesis, the
164
+ # norm for a VO-typed aggregate field) is exactly the shape
165
+ # #rewrap_arithmetic_result hands back a raw scalar RESULT to
166
+ # -- without this, every phantom-field increment/multiply on a
167
+ # single-field-wrapped attribute refused with "pass its fields
168
+ # as an object, not <scalar>" the instant it tried to re-wrap
169
+ # its own correctly-computed result. Multi-field VOs still
170
+ # refuse below, unchanged -- only the genuinely unambiguous
171
+ # single-field case auto-wraps, matching from_identifier's own
172
+ # precedent exactly.
173
+ if value_object.attributes.size == 1
174
+ return { value_object.attributes.first.name => value }
175
+ end
176
+
177
+ raise TypeMismatch,
178
+ RefusalWording.render("TypeMismatch", "value_object_shape",
179
+ name: name, type: value_object.hecks_name,
180
+ offered: Rendering.describe(value))
181
+ end
182
+
183
+ # `build`'s own recursive twin of `for_attribute`'s single-level
184
+ # normalization — a value object's OWN composite-typed fields
185
+ # (`Pizza.price_cents`, a `Price`) never otherwise pass back
186
+ # through `fields_for`, so a bare scalar or partial Hash for one
187
+ # of THOSE sails past the outer VO's own shape check (`Pizza`
188
+ # itself has two fields, so nothing unwraps there) and lands
189
+ # stored one field down exactly as handed in — found live: once
190
+ # the fuzzer actually generated the bare-scalar shape
191
+ # `fields_for` has accepted at the TOP level since 86727afd, a
192
+ # nested `Price` stored as a raw Integer broke every later
193
+ # dotted-path read (`pizza.price_cents.cents`) expecting one
194
+ # more level of Hash.
195
+ #
196
+ # Stays a plain Hash, never a nested `Value` — `Value#with`'s own
197
+ # header and `materialize_unwrapped`'s comment already depend on
198
+ # a value-object-typed field of ANOTHER value object staying a
199
+ # plain Hash once stored, and this does not change that; it only
200
+ # makes sure that Hash has the shape its own type declares.
201
+ # `aggregate` is the one thing `build` didn't used to need — a
202
+ # nested type can only be resolved through `aggregate.
203
+ # value_object(name)`, so callers with no aggregate in reach
204
+ # (`Value#with`, always re-setting an already-scalar arithmetic
205
+ # field) simply skip this and keep their prior behavior.
206
+ # RECURSES INTO EACH NESTED FIELD'S OWN VALIDATION TOO, not only its
207
+ # shape — found live alongside the shape bug this method's header
208
+ # already describes: a nested `Price`/`Size` (a value-object-typed
209
+ # field of ANOTHER value object, e.g. `Pizza.price_cents`,
210
+ # `Pizza.size`) had its Hash shape normalized here but never ran
211
+ # `validate!` — `build`, below, only ever validated the OUTER value
212
+ # object's own direct fields, so a negative `price_cents.cents` or an
213
+ # out-of-`one_of` `size.value` sailed through a `Pizza`-typed command
214
+ # argument untouched, while the exact same nested type declared as a
215
+ # direct, top-level command attribute (`SafeDepositBox.Rent`'s own
216
+ # `attribute :size, Size`) was already checked correctly. `apply_
217
+ # defaults` runs first, same as the outer value object gets in
218
+ # `build`, so a nested field's own default is filled in before its
219
+ # own invariants read it.
220
+ def normalize_composite_fields(aggregate, value_object, fields)
221
+ return fields unless aggregate&.respond_to?(:value_object)
222
+
223
+ value_object.attributes.each do |attribute|
224
+ next if attribute.list? || !fields.key?(attribute.name)
225
+
226
+ raw = fields[attribute.name]
227
+ next if raw.nil? || raw.is_a?(self)
228
+
229
+ nested = value_object_for(aggregate, attribute.type)
230
+ next unless nested
231
+
232
+ nested_fields = apply_defaults(nested, fields_for(nested, attribute.name, raw))
233
+ nested_fields = normalize_composite_fields(aggregate, nested, nested_fields)
234
+ validate!(nested, nested_fields)
235
+ fields[attribute.name] = nested_fields
236
+ end
237
+
238
+ fields
239
+ end
240
+
241
+ def apply_defaults(value_object, fields)
242
+ value_object.attributes.each_with_object(fields) do |attribute, completed|
243
+ completed[attribute.name] = attribute.default unless completed.key?(attribute.name) || attribute.default.nil?
244
+ end
245
+ end
246
+
247
+ # THE FULL DOOR A VALUE OBJECT'S OWN FIELDS PASS THROUGH — shared by
248
+ # `build` (the outer value object) and `normalize_composite_fields`
249
+ # (every nested one), so a nested `Price`/`Size` is refused exactly
250
+ # the same way, with exactly the same wording, as the identical type
251
+ # declared directly on a command.
252
+ def validate!(value_object, fields)
253
+ admit_member(value_object, fields)
254
+ check_admitted(value_object, fields)
255
+ check_numeric_fields(value_object, fields)
256
+ check_scalar_shapes(value_object, fields)
257
+ check_patterns(value_object, fields)
258
+ value_object.invariants.each do |invariant|
259
+ next if Bluebook::Expression::Evaluator.call(invariant.canonical, fields)
260
+
261
+ raise InvariantViolation,
262
+ RefusalWording.render("InvariantViolation", "value_object_invariant",
263
+ name: value_object.hecks_name, description: invariant.description,
264
+ offered: canonical_fields(fields))
265
+ end
266
+ end
267
+
268
+ def build(value_object, fields, aggregate = nil)
269
+ fields = apply_defaults(value_object, fields.transform_keys(&:to_sym))
270
+ fields = normalize_composite_fields(aggregate, value_object, fields)
271
+ validate!(value_object, fields)
272
+ new(value_object, fields)
273
+ end
274
+
275
+ def hydrate(aggregate, state)
276
+ state.each_with_object({}) do |(name, value), hydrated|
277
+ key = name.to_sym
278
+ attribute = aggregate.attribute(key)
279
+ hydrated[key] = attribute ? for_attribute(aggregate, attribute, value) : value
280
+ end
281
+ end
282
+
283
+ # S17, ADR 0026 — SEARCHES THE WHOLE ENTITY TREE, not only the
284
+ # root's own direct children. `aggregate` here is always the
285
+ # ROOT aggregate — `for_attribute`'s own `aggregate` argument is
286
+ # never reassigned as hydration recurses into a nested element,
287
+ # because coercion has to resolve value objects, and only the
288
+ # root answers `.value_object` at all (Entity's own header
289
+ # comment: an entity must NOT answer to it, or `Value.
290
+ # for_attribute` could no longer tell a piece from a head). So
291
+ # a NESTED entity — Dispatch, inside Handler — is not a direct
292
+ # child of the root the way Handler itself is, and a plain
293
+ # `aggregate.entities.find` stops one level short of it.
294
+ def find_entity(construct, name)
295
+ construct.entities.each do |candidate|
296
+ return candidate if candidate.hecks_name == name
297
+
298
+ found = find_entity(candidate, name)
299
+ return found if found
300
+ end
301
+ nil
302
+ end
303
+
304
+ # Frozen through: a list read back out of the store is an answer,
305
+ # not a handle on what is stored.
306
+ def hydrate_entity_list(aggregate, attribute, value)
307
+ entity = find_entity(aggregate, attribute.type.to_s)
308
+ return value unless entity
309
+
310
+ Array(value).map do |element|
311
+ next element unless element.is_a?(Hash)
312
+
313
+ element.each_with_object({}) do |(name, field_value), hydrated|
314
+ key = name.to_sym
315
+ field = entity.attribute(key)
316
+ hydrated[key] = field ? for_attribute(aggregate, field, field_value) : field_value
317
+ end
318
+ end
319
+ .then { |hydrated| Freezer.deep(hydrated) }
320
+ end
321
+
322
+ # `Value.identifier` used to live here: hand it a one-field value object
323
+ # and it opened it, so `identified_by :number` could pass for an identity
324
+ # and the runtime would guess which field was meant. THAT GUESS IS GONE.
325
+ # An identity names its field — `identified_by :number` — and the
326
+ # path is what reaches the scalar. A declaration that names no field is
327
+ # refused when the bluebook loads, so nothing has to be unwrapped later.
328
+ #
329
+ # `scalar` below is a different job and stays: rendering a value object
330
+ # into a column or a message, where there is no path to consult.
331
+
332
+ # `Value.reference_id` lived here, opening a reference to find the id
333
+ # inside it. A reference IS the id now — refused at the payload gate if it
334
+ # arrives as anything else — so there is nothing left to open. The comment
335
+ # it carried said retiring it meant changing how references are STORED ;
336
+ # that is what happened.
337
+
338
+ # A REFERENCE IS AN ID, SO AN OBJECT IS NOT ONE.
339
+ #
340
+ # Nothing coerces a reference — `for_attribute` misses on
341
+ # "Reference<Account>", which is no value object's name, and hands the
342
+ # argument straight through. That is why the wrapped form went in
343
+ # unnoticed for as long as it did: there was no place it could be
344
+ # refused, so whatever the first caller wrote became the shape.
345
+ #
346
+ # This is that place. It sits at the payload gate rather than inside
347
+ # coercion because the sentence names the COMMAND, and `for_attribute`
348
+ # never learns which command it is serving.
349
+ #
350
+ # An Array is deliberately not refused here. A reference is never a list
351
+ # today, and inventing a rule for a shape the language cannot declare is
352
+ # how decoration gets written.
353
+ def refuse_object_reference(command, attribute, value)
354
+ return unless attribute.reference?
355
+
356
+ offered = attribute.list? ? Array(value).find { |item| item.is_a?(Hash) || item.is_a?(self) } : value
357
+ return unless offered.is_a?(Hash) || offered.is_a?(self)
358
+
359
+ raise TypeMismatch,
360
+ RefusalWording.render("TypeMismatch", "reference_as_object",
361
+ command: command.hecks_name, attribute: attribute.name,
362
+ known_by: known_by(attribute))
363
+ end
364
+
365
+ # "(Account is known by number)" — what to send instead. No article, on
366
+ # purpose: "an Account" and "a Customer" differ by the target's first
367
+ # letter, and a refusal pinned byte-for-byte should not hinge on an
368
+ # article-choosing rule. Silent when the target is another chapter's,
369
+ # where this runtime cannot see what it is known by.
370
+ #
371
+ # EVERY HEAD, because a caller has to pass every one. This read
372
+ # `identified_by`, which is the SINGLE head and is nil the moment an
373
+ # identity has two parts — so a composite target fell through the guard
374
+ # and the refusal went silent exactly where it had the most to say. A
375
+ # single-path target reads as it always did.
376
+ def known_by(attribute)
377
+ heads = Array(attribute.type.resolve&.identity_heads)
378
+ return "" if heads.empty?
379
+
380
+ " (#{attribute.type.target_name} is known by #{heads.join(', ')})"
381
+ end
382
+
383
+ def scalar(value)
384
+ return value unless value.is_a?(self)
385
+
386
+ fields = value.to_h
387
+ return fields.values.first if fields.size == 1
388
+
389
+ raise TypeMismatch, RefusalWording.render("TypeMismatch", "multi_field_scalar", type: value.type_name)
390
+ end
391
+
392
+ def from_identifier(aggregate, attribute, identifier)
393
+ value_object = value_object_for(aggregate, attribute.type)
394
+ return identifier unless value_object
395
+
396
+ fields = value_object.attributes
397
+ if fields.size == 1
398
+ field = fields.first
399
+ return build(value_object, { field.name => coerce_identifier(field, identifier) })
400
+ end
401
+
402
+ raise TypeMismatch, RefusalWording.render("TypeMismatch", "composite_identity", type: value_object.hecks_name)
403
+ end
404
+
405
+ # Vendored fix, not (yet) upstream hecks (migration plan
406
+ # task 9): `identifier` here is always the DERIVED IDENTITY
407
+ # STRING -- `Identity.of`/`Identity.from` intentionally return
408
+ # one (correct for naming a repository key), and
409
+ # `Runtime::Instance#materialize_identity!` calls `from_identifier`
410
+ # with exactly that string on every fresh hydration -- but when
411
+ # the identity field's OWN declared type is Integer/Float (not
412
+ # the overwhelmingly common String), seeding it straight from
413
+ # that string round-trips a correctly-derived identity back in
414
+ # as the WRONG Ruby type -- and #build's own
415
+ # `check_numeric_fields` (added specifically to catch a genuine
416
+ # CALLER mismatch) then refused the runtime's OWN internal
417
+ # identity seed instead, on every dispatch, valid input or not.
418
+ #
419
+ # Reuses THIS SAME FILE's own `NUMERIC` table (declared-type ->
420
+ # expected-Ruby-class, already read by `check_numeric_fields`)
421
+ # to decide WHICH declared types need converting, and
422
+ # Kernel#Integer/#Float to do the converting. A genuinely
423
+ # malformed identifier (should never happen, since an identity
424
+ # is always derived FROM a correctly-typed field in the first
425
+ # place, but this stays defensive rather than assume it) passes
426
+ # back unconverted, and `check_numeric_fields` refuses it
427
+ # exactly as it always has -- preserving its real job of
428
+ # catching a genuine caller mismatch, not just this migration's
429
+ # own runtime-internal one.
430
+ private def coerce_identifier(field, identifier)
431
+ return identifier unless identifier.is_a?(String) && NUMERIC.key?(field.type.to_s)
432
+
433
+ case field.type.to_s
434
+ when "Integer" then Integer(identifier)
435
+ when "Float" then Float(identifier)
436
+ else identifier
437
+ end
438
+ rescue ArgumentError
439
+ identifier
440
+ end
441
+
442
+ def canonical_fields(fields)
443
+ JSON.generate(fields.sort_by { |name, _| name.to_s }.to_h)
444
+ end
445
+
446
+ # A field declared Integer or Float must ARRIVE as one.
447
+ #
448
+ # Without this a String sails into a numeric field and the failure surfaces
449
+ # later, inside a predicate, as `positive? expects a number, got "three"` —
450
+ # an EvaluationError, which is NOT a domain refusal. So the runtime broke
451
+ # where the domain should have said no, and the run contract recorded the
452
+ # crash beside genuine refusals as though the domain had judged it.
453
+ #
454
+ # Checked BEFORE invariants, because an invariant reading a mistyped field
455
+ # is exactly the thing that used to explode.
456
+ NUMERIC = { "Integer" => Integer, "Float" => Numeric }.freeze
457
+ private def check_numeric_fields(value_object, fields)
458
+ value_object.attributes.each do |attribute|
459
+ expected = NUMERIC[attribute.type.to_s]
460
+ next unless expected
461
+
462
+ given = fields[attribute.name]
463
+ next if given.nil?
464
+
465
+ unless given.is_a?(expected)
466
+ raise TypeMismatch,
467
+ RefusalWording.render("TypeMismatch", "numeric_field",
468
+ type: value_object.hecks_name, field: attribute.name,
469
+ expected: attribute.type, offered: Rendering.describe(given))
470
+ end
471
+
472
+ # PRD 05 (numeric-boundary-coverage) — `given.is_a?(expected)`
473
+ # alone waves a NaN or an Infinity straight through: both are
474
+ # real `Float`s, so `is_a?(Numeric)`/`is_a?(Float)` is true for
475
+ # either. Never exercised before this, because
476
+ # `ValueGenerator::FLOAT_EDGE_CASES` had no non-finite value in
477
+ # it — the fuzzer could not have found this on its own until
478
+ # the table was widened alongside this fix. Left unchecked, a
479
+ # non-finite Float reaches `CommandRules::Arithmetic#clamp`
480
+ # (`current.clamp(min, max)` — `ArgumentError: comparison of
481
+ # Float with X failed`, a genuine Ruby-level crash, not a
482
+ # domain refusal, exactly the same failure mode this method's
483
+ # own header describes for a mistyped field) or all the way to
484
+ # storage, where `JSON.generate`/`#to_json` raises
485
+ # `JSON::GeneratorError: NaN/Infinity not allowed in JSON` the
486
+ # moment anything tries to persist or replay it — again a raw
487
+ # crash, not a refusal. `-0.0` is deliberately NOT refused
488
+ # here: it IS finite, round-trips through JSON as `-0.0`
489
+ # cleanly (confirmed empirically), and is a legitimate,
490
+ # meaningful float value (a signed zero), not a corruption
491
+ # risk — only NaN and +/-Infinity are.
492
+ if given.is_a?(Float) && !given.finite?
493
+ raise TypeMismatch,
494
+ RefusalWording.render("TypeMismatch", "non_finite_field",
495
+ type: value_object.hecks_name, field: attribute.name,
496
+ offered: Rendering.describe(given))
497
+ end
498
+ end
499
+ end
500
+
501
+ # A field declared `String` (or a boolean) must not arrive as a
502
+ # COMPOSITE — an Array or a Hash (or a nested Value) standing in for
503
+ # what has to be a leaf scalar.
504
+ #
505
+ # Deliberately laxer than `check_numeric_fields` above : it does not
506
+ # enforce the exact Ruby class, only that the shape isn't a collection.
507
+ # `Judge#v` — the language's own self-hosted grammar validation —
508
+ # hands a String-typed field (`Normalise`'s `position`, a `RuleText`)
509
+ # a raw Integer walk-index on purpose, on every boot, and that has
510
+ # always been tolerated ; a full String-vs-Integer check here would
511
+ # refuse the runtime's own bootstrap. But no scalar field, of any
512
+ # declared type, can ever legitimately be handed an Array or a Hash —
513
+ # that shape is always wrong, and always was: `InvalidValueGenerator#
514
+ # array_for_scalar`'s own corruption is deliberately built to be
515
+ # REFUSED (see that file's header), and until this check existed it
516
+ # sailed straight through for a String/boolean field the way it never
517
+ # could for an Integer/Float one (`check_numeric_fields` above already
518
+ # catches an Array offered for those). Found live via bin/fuzz, seed
519
+ # 17 on the fixtures domain : an Array standing in for a single-field
520
+ # identity's declared `String`, `.to_s`'d into a record id downstream.
521
+ COMPOSITE_SHAPES = [Array, ::Hash].freeze
522
+ NON_NUMERIC_SCALARS = %w[String TrueClass FalseClass].freeze
523
+ private def check_scalar_shapes(value_object, fields)
524
+ value_object.attributes.each do |attribute|
525
+ next unless NON_NUMERIC_SCALARS.include?(attribute.type.to_s)
526
+
527
+ given = fields[attribute.name]
528
+ next if given.nil? || COMPOSITE_SHAPES.none? { |shape| given.is_a?(shape) }
529
+
530
+ raise TypeMismatch,
531
+ RefusalWording.render("TypeMismatch", "numeric_field",
532
+ type: value_object.hecks_name, field: attribute.name,
533
+ expected: attribute.type, offered: Rendering.describe(given))
534
+ end
535
+ end
536
+
537
+ # A field declared with a PATTERN must match it.
538
+ #
539
+ # Beside check_numeric_fields and for the same reason : a value that does
540
+ # not look like what it claims to be is the DOMAIN saying no, and it should
541
+ # say so here rather than let the wrong shape travel on and surface as a
542
+ # broken predicate later.
543
+ #
544
+ # Which regexes may be written at all is PatternSubset's job, enforced when
545
+ # the bluebook is declared — so by the time a value arrives here the pattern
546
+ # is already a vetted, unambiguous one, and this is a plain match.
547
+ private def check_patterns(value_object, fields)
548
+ value_object.attributes.each do |attribute|
549
+ pattern = attribute.pattern
550
+ next unless pattern
551
+
552
+ given = fields[attribute.name]
553
+ next if given.nil?
554
+ next if given.is_a?(String) && Regexp.new(pattern).match?(given)
555
+
556
+ raise TypeMismatch,
557
+ RefusalWording.render("TypeMismatch", "pattern_mismatch",
558
+ type: value_object.hecks_name, field: attribute.name,
559
+ pattern: pattern, offered: Rendering.describe(given))
560
+ end
561
+ end
562
+ end
563
+ end
564
+ end
565
+ end
@@ -0,0 +1,5 @@
1
+ module Hecks
2
+ module Runtime
3
+ class InvariantViolation < StandardError; end
4
+ end
5
+ end