hecks 2.8.0 → 2.9.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 (404) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +4 -6
  3. data/lib/hecks/adapters/driven/claude_code.rb +4 -104
  4. data/lib/hecks/adapters/driven/d1.rb +44 -237
  5. data/lib/hecks/adapters/driven/folder.rb +17 -76
  6. data/lib/hecks/adapters/driven/google_authentication.rb +15 -49
  7. data/lib/hecks/adapters/driven/governance_authorization.rb +23 -70
  8. data/lib/hecks/adapters/driven/heki/journal.rb +8 -41
  9. data/lib/hecks/adapters/driven/heki/saga_store.rb +12 -54
  10. data/lib/hecks/adapters/driven/heki/snapshot.rb +4 -12
  11. data/lib/hecks/adapters/driven/heki.rb +13 -42
  12. data/lib/hecks/adapters/driven/identity_registry.rb +3 -14
  13. data/lib/hecks/adapters/driven/in_memory_ordering.rb +9 -26
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +2 -14
  15. data/lib/hecks/adapters/driven/lambda/client.rb +10 -51
  16. data/lib/hecks/adapters/driven/lambda.rb +10 -84
  17. data/lib/hecks/adapters/driven/local_storage.rb +5 -40
  18. data/lib/hecks/adapters/driven/memory.rb +17 -98
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +5 -21
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +6 -28
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +3 -12
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +4 -30
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +34 -108
  24. data/lib/hecks/adapters/driven/postgres.rb +145 -134
  25. data/lib/hecks/adapters/driven/postgres_era.adapter +4 -15
  26. data/lib/hecks/adapters/driven/prism.rb +7 -30
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +21 -121
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +8 -40
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +26 -82
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +40 -84
  31. data/lib/hecks/adapters/driven/sqlite.rb +128 -65
  32. data/lib/hecks/adapters/driven/tenant_provisioner.rb +5 -48
  33. data/lib/hecks/adapters/driven.rb +5 -36
  34. data/lib/hecks/adapters/driving/github_webhook.rb +8 -76
  35. data/lib/hecks/behaviors/dsl.rb +7 -25
  36. data/lib/hecks/behaviors/expectations.rb +31 -141
  37. data/lib/hecks/behaviors/ir.rb +7 -20
  38. data/lib/hecks/behaviors/rspec.rb +6 -17
  39. data/lib/hecks/behaviors/runner.rb +10 -34
  40. data/lib/hecks/behaviors.rb +4 -8
  41. data/lib/hecks/bluebook/aggregate.rb +9 -75
  42. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +9 -38
  43. data/lib/hecks/bluebook/assembly/build.rb +4 -15
  44. data/lib/hecks/bluebook/assembly/contract.rb +15 -136
  45. data/lib/hecks/bluebook/assembly/contracts.rb +54 -174
  46. data/lib/hecks/bluebook/assembly/marks.rb +31 -223
  47. data/lib/hecks/bluebook/assembly/specializer.rb +6 -52
  48. data/lib/hecks/bluebook/assembly.rb +6 -45
  49. data/lib/hecks/bluebook/attribute.rb +12 -61
  50. data/lib/hecks/bluebook/behaviour/aggregate.rb +10 -39
  51. data/lib/hecks/bluebook/behaviour/attribute.rb +3 -14
  52. data/lib/hecks/bluebook/behaviour/chapter.rb +14 -42
  53. data/lib/hecks/bluebook/behaviour/command.rb +14 -66
  54. data/lib/hecks/bluebook/behaviour/domain_port.rb +4 -20
  55. data/lib/hecks/bluebook/behaviour/entity.rb +7 -27
  56. data/lib/hecks/bluebook/behaviour/hexagon.rb +23 -41
  57. data/lib/hecks/bluebook/behaviour/lifecycle.rb +15 -40
  58. data/lib/hecks/bluebook/behaviour/policy.rb +23 -52
  59. data/lib/hecks/bluebook/behaviour/process_manager.rb +30 -57
  60. data/lib/hecks/bluebook/behaviour/read_model.rb +25 -31
  61. data/lib/hecks/bluebook/behaviour/traits.rb +27 -64
  62. data/lib/hecks/bluebook/behaviour/value_object.rb +5 -24
  63. data/lib/hecks/bluebook/capabilities.rb +17 -49
  64. data/lib/hecks/bluebook/chapter.rb +10 -48
  65. data/lib/hecks/bluebook/command.rb +23 -81
  66. data/lib/hecks/bluebook/domain_port.rb +14 -63
  67. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +50 -229
  68. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +50 -377
  69. data/lib/hecks/bluebook/dsl/attribute_collector.rb +33 -204
  70. data/lib/hecks/bluebook/dsl/binding_proxy.rb +16 -69
  71. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +86 -708
  72. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +56 -170
  73. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +1 -0
  74. data/lib/hecks/bluebook/dsl/command_builder.rb +74 -546
  75. data/lib/hecks/bluebook/dsl/const_shim.rb +8 -41
  76. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +12 -87
  77. data/lib/hecks/bluebook/dsl/entity_builder.rb +44 -330
  78. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +20 -269
  79. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +26 -194
  80. data/lib/hecks/bluebook/dsl/identity_declaration.rb +13 -88
  81. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +6 -20
  82. data/lib/hecks/bluebook/dsl/policy_builder.rb +16 -88
  83. data/lib/hecks/bluebook/dsl/port_builder.rb +2 -6
  84. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +22 -96
  85. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +31 -184
  86. data/lib/hecks/bluebook/dsl/query_builder.rb +11 -42
  87. data/lib/hecks/bluebook/dsl/read_model_builder.rb +75 -201
  88. data/lib/hecks/bluebook/dsl/rule_reference.rb +22 -163
  89. data/lib/hecks/bluebook/dsl/translation_builder.rb +28 -114
  90. data/lib/hecks/bluebook/dsl/value_object_builder.rb +10 -124
  91. data/lib/hecks/bluebook/dsl/word_gate.rb +16 -162
  92. data/lib/hecks/bluebook/dsl/world_builder.rb +19 -111
  93. data/lib/hecks/bluebook/entity.rb +13 -71
  94. data/lib/hecks/bluebook/expression/ast_json.rb +24 -190
  95. data/lib/hecks/bluebook/expression/ast_reader.rb +12 -54
  96. data/lib/hecks/bluebook/expression/canonical_form.rb +11 -88
  97. data/lib/hecks/bluebook/expression/evaluator.rb +47 -295
  98. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +30 -147
  99. data/lib/hecks/bluebook/expression/resolver.rb +37 -660
  100. data/lib/hecks/bluebook/hexagon.rb +5 -28
  101. data/lib/hecks/bluebook/lifecycle.rb +5 -16
  102. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -7
  103. data/lib/hecks/bluebook/meta_validator/judge.rb +111 -361
  104. data/lib/hecks/bluebook/meta_validator/plan.rb +33 -162
  105. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -7
  106. data/lib/hecks/bluebook/meta_validator/readings.rb +56 -342
  107. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -176
  108. data/lib/hecks/bluebook/meta_validator/shapes.rb +53 -267
  109. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +43 -305
  110. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +7 -43
  111. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -18
  112. data/lib/hecks/bluebook/meta_validator.rb +79 -349
  113. data/lib/hecks/bluebook/model_check/client_profile.rb +6 -21
  114. data/lib/hecks/bluebook/model_check.rb +100 -597
  115. data/lib/hecks/bluebook/pattern_subset.rb +11 -69
  116. data/lib/hecks/bluebook/process_manager.rb +13 -83
  117. data/lib/hecks/bluebook/project_loader.rb +3 -39
  118. data/lib/hecks/bluebook/project_register.rb +12 -60
  119. data/lib/hecks/bluebook/query.rb +9 -48
  120. data/lib/hecks/bluebook/read_model.rb +57 -81
  121. data/lib/hecks/bluebook/reference.rb +7 -41
  122. data/lib/hecks/bluebook/smoke_test.rb +8 -98
  123. data/lib/hecks/bluebook/synthesizer.rb +13 -62
  124. data/lib/hecks/bluebook/translation.rb +26 -62
  125. data/lib/hecks/bluebook/value_object.rb +11 -36
  126. data/lib/hecks/bluebook.rb +5 -22
  127. data/lib/hecks/cache_dir.rb +4 -23
  128. data/lib/hecks/cli/document.rb +3 -21
  129. data/lib/hecks/cli/ir.rb +3 -5
  130. data/lib/hecks/cli/mcp.rb +2 -3
  131. data/lib/hecks/cli/mcp_door.rb +19 -114
  132. data/lib/hecks/cli/model_check.rb +18 -85
  133. data/lib/hecks/cli/project_cli.rb +7 -40
  134. data/lib/hecks/cli/project_diagrams.rb +0 -7
  135. data/lib/hecks/cli/run.rb +12 -58
  136. data/lib/hecks/cli/smoke_test.rb +2 -16
  137. data/lib/hecks/cli/stores.rb +1 -2
  138. data/lib/hecks/cli.rb +18 -24
  139. data/lib/hecks/construct.rb +8 -34
  140. data/lib/hecks/deploy/bluebook/deploy.bluebook +33 -142
  141. data/lib/hecks/deploy/bluebook/deploy.hecksagon +2 -9
  142. data/lib/hecks/embryonaut_bluebook/lock.rb +3 -35
  143. data/lib/hecks/embryonaut_bluebook/shape.rb +1 -17
  144. data/lib/hecks/embryonaut_bluebook/vendor.rb +8 -53
  145. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +4 -19
  146. data/lib/hecks/embryonaut_bluebook.rb +4 -71
  147. data/lib/hecks/facade/cli_door.rb +24 -106
  148. data/lib/hecks/facade/cli_runner.rb +28 -172
  149. data/lib/hecks/facade/command_request.rb +13 -33
  150. data/lib/hecks/facade/handle.rb +34 -121
  151. data/lib/hecks/facade/json_door.rb +31 -146
  152. data/lib/hecks/facade/surface/aggregate_door.rb +39 -104
  153. data/lib/hecks/facade/surface/chapter.rb +10 -54
  154. data/lib/hecks/facade/surface.rb +8 -23
  155. data/lib/hecks/facade.rb +10 -25
  156. data/lib/hecks/forms/app.rb +15 -70
  157. data/lib/hecks/forms/command_form_renderer.rb +10 -29
  158. data/lib/hecks/forms/field_renderer.rb +17 -156
  159. data/lib/hecks/forms/field_shape.rb +30 -222
  160. data/lib/hecks/forms/html.rb +12 -47
  161. data/lib/hecks/forms/index_renderer.rb +1 -4
  162. data/lib/hecks/forms/page.rb +3 -9
  163. data/lib/hecks/forms/params.rb +14 -60
  164. data/lib/hecks/forms/port_argument.rb +2 -19
  165. data/lib/hecks/forms/query_form_renderer.rb +4 -79
  166. data/lib/hecks/forms/record_renderer.rb +8 -75
  167. data/lib/hecks/forms/record_table.rb +7 -50
  168. data/lib/hecks/forms/reference_options.rb +3 -30
  169. data/lib/hecks/forms/value_object_shape.rb +8 -35
  170. data/lib/hecks/forms.rb +5 -33
  171. data/lib/hecks/fqn.rb +5 -12
  172. data/lib/hecks/framework/bluebook/compliance.bluebook +16 -65
  173. data/lib/hecks/framework/bluebook/console_settings.bluebook +47 -134
  174. data/lib/hecks/framework/bluebook/framework.hecksagon +5 -14
  175. data/lib/hecks/framework/bluebook/governance.bluebook +13 -34
  176. data/lib/hecks/framework/bluebook/identity.bluebook +11 -26
  177. data/lib/hecks/framework/bluebook/privacy.bluebook +8 -33
  178. data/lib/hecks/framework.rb +11 -80
  179. data/lib/hecks/freezer.rb +5 -39
  180. data/lib/hecks/grammar/expression.bluebook +15 -40
  181. data/lib/hecks/grammar/translation.bluebook +3 -6
  182. data/lib/hecks/grammar.rb +16 -41
  183. data/lib/hecks/ir.rb +14 -83
  184. data/lib/hecks/language/adapter.bluebook +2 -7
  185. data/lib/hecks/language/bluebook/aggregate.bluebook +56 -221
  186. data/lib/hecks/language/bluebook/attaches/paging.bluebook +6 -15
  187. data/lib/hecks/language/bluebook/bluebook.bluebook +15 -83
  188. data/lib/hecks/language/bluebook/bluebook.hecksagon +3 -15
  189. data/lib/hecks/language/bluebook/command.bluebook +27 -144
  190. data/lib/hecks/language/bluebook/entity.bluebook +24 -97
  191. data/lib/hecks/language/bluebook/policy.bluebook +16 -67
  192. data/lib/hecks/language/bluebook/process_manager.bluebook +28 -151
  193. data/lib/hecks/language/bluebook/projection.bluebook +126 -60
  194. data/lib/hecks/language/bluebook/query.bluebook +12 -43
  195. data/lib/hecks/language/bluebook/shape.bluebook +23 -73
  196. data/lib/hecks/language/bluebook/syntax.bluebook +51 -262
  197. data/lib/hecks/language/bluebook/vocabulary.bluebook +82 -197
  198. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +3 -10
  199. data/lib/hecks/language/hecksagon/domain_port.bluebook +4 -19
  200. data/lib/hecks/language/hecksagon/hecksagon.bluebook +6 -25
  201. data/lib/hecks/language/hecksagon/port_operation.bluebook +3 -14
  202. data/lib/hecks/language/oidc.json +35 -0
  203. data/lib/hecks/language/port.bluebook +4 -17
  204. data/lib/hecks/language/translation/translation.bluebook +3 -9
  205. data/lib/hecks/language/translation/translation_aggregate.bluebook +5 -24
  206. data/lib/hecks/language/world/wiring.bluebook +1 -3
  207. data/lib/hecks/language/world/world.bluebook +5 -7
  208. data/lib/hecks/literal.rb +8 -50
  209. data/lib/hecks/mcp_door_scope.rb +9 -90
  210. data/lib/hecks/mcp_stdio_guard.rb +4 -41
  211. data/lib/hecks/naming.rb +29 -137
  212. data/lib/hecks/ports/access_control.rb +7 -23
  213. data/lib/hecks/ports/agent/answers.rb +5 -23
  214. data/lib/hecks/ports/agent.rb +47 -172
  215. data/lib/hecks/ports/authentication.rb +12 -39
  216. data/lib/hecks/ports/authorization.rb +16 -43
  217. data/lib/hecks/ports/clock.rb +5 -45
  218. data/lib/hecks/ports/extraction.rb +7 -12
  219. data/lib/hecks/ports/identity_assignment.rb +8 -29
  220. data/lib/hecks/ports/identity_generation.rb +6 -29
  221. data/lib/hecks/ports/identity_resolution.rb +2 -10
  222. data/lib/hecks/ports/key_vault.rb +9 -45
  223. data/lib/hecks/ports/persistence/append_only.rb +61 -64
  224. data/lib/hecks/ports/persistence/binding_policy.rb +4 -14
  225. data/lib/hecks/ports/persistence/codec_boundary.rb +20 -80
  226. data/lib/hecks/ports/persistence/null_saga_store.rb +4 -20
  227. data/lib/hecks/ports/persistence/plugin.rb +13 -42
  228. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +38 -141
  229. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +15 -115
  230. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +16 -84
  231. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +6 -25
  232. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +3 -22
  233. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +55 -181
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/events.rb +77 -0
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +12 -122
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +28 -128
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +73 -474
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +31 -124
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +45 -219
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +13 -108
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +18 -75
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +9 -49
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +6 -123
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +23 -55
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +14 -72
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +7 -16
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +14 -115
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +13 -45
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +127 -554
  250. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +13 -63
  251. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +2 -17
  252. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +3 -14
  253. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +13 -58
  254. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -17
  255. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +9 -26
  256. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +9 -25
  257. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +18 -124
  258. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +34 -123
  259. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +5 -12
  260. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +3 -9
  261. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -14
  262. data/lib/hecks/ports/persistence/plugins/era.rb +4 -23
  263. data/lib/hecks/ports/persistence/postgres_dump/connection.rb +7 -23
  264. data/lib/hecks/ports/persistence/postgres_dump.rb +4 -35
  265. data/lib/hecks/ports/persistence/remote_runtime.rb +2 -24
  266. data/lib/hecks/ports/persistence/repository_factory.rb +8 -27
  267. data/lib/hecks/ports/persistence/state_codec.rb +23 -103
  268. data/lib/hecks/ports/persistence.rb +7 -14
  269. data/lib/hecks/ports/projection.rb +42 -84
  270. data/lib/hecks/ports/query/in_memory.rb +10 -58
  271. data/lib/hecks/ports/query/ordering.rb +11 -32
  272. data/lib/hecks/ports/query.rb +14 -37
  273. data/lib/hecks/ports.rb +2 -8
  274. data/lib/hecks/projections/bootstrap_table.rb +11 -43
  275. data/lib/hecks/projections/deploy/fargate/assembly.rb +2 -15
  276. data/lib/hecks/projections/deploy/fargate/cdn.rb +1 -48
  277. data/lib/hecks/projections/deploy/fargate/cdn_default.rb +2 -11
  278. data/lib/hecks/projections/deploy/fargate/check.rb +5 -14
  279. data/lib/hecks/projections/deploy/fargate/containers.rb +3 -78
  280. data/lib/hecks/projections/deploy/fargate/extras.rb +3 -44
  281. data/lib/hecks/projections/deploy/fargate/monitoring.rb +0 -44
  282. data/lib/hecks/projections/deploy/fargate/settings.rb +2 -54
  283. data/lib/hecks/projections/deploy/fargate/yaml.rb +13 -58
  284. data/lib/hecks/projections/deploy/fargate.rb +35 -202
  285. data/lib/hecks/projections/deploy/lambda.rb +150 -678
  286. data/lib/hecks/projections/deploy/preview/access_yaml.rb +1 -33
  287. data/lib/hecks/projections/deploy/preview/containers.rb +6 -111
  288. data/lib/hecks/projections/deploy/preview/routing_yaml.rb +4 -56
  289. data/lib/hecks/projections/deploy/preview/script.rb +2 -14
  290. data/lib/hecks/projections/deploy/preview/settings.rb +3 -33
  291. data/lib/hecks/projections/deploy/preview/task_yaml.rb +2 -38
  292. data/lib/hecks/projections/deploy/preview/template.rb +2 -58
  293. data/lib/hecks/projections/deploy/preview.rb +2 -67
  294. data/lib/hecks/projections/deploy/scripts/settings.rb +3 -25
  295. data/lib/hecks/projections/deploy/scripts.rb +4 -57
  296. data/lib/hecks/projections/deploy/shared.rb +47 -146
  297. data/lib/hecks/projections/deploy/smoke.rb +4 -61
  298. data/lib/hecks/projections/deploy/template_diff/comparer.rb +3 -25
  299. data/lib/hecks/projections/deploy/template_diff/loader.rb +2 -11
  300. data/lib/hecks/projections/deploy/template_diff.rb +5 -57
  301. data/lib/hecks/projections/diagrams.rb +20 -582
  302. data/lib/hecks/projections/glossary/html.rb +14 -130
  303. data/lib/hecks/projections/glossary/markdown.rb +8 -69
  304. data/lib/hecks/projections/glossary/mermaid.rb +9 -66
  305. data/lib/hecks/projections/glossary/sensitivity.rb +5 -34
  306. data/lib/hecks/projections/glossary/sentences.rb +14 -159
  307. data/lib/hecks/projections/glossary.rb +30 -222
  308. data/lib/hecks/projections/ir.rb +3 -10
  309. data/lib/hecks/projections/model/deviations.rb +15 -42
  310. data/lib/hecks/projections/model.rb +12 -51
  311. data/lib/hecks/projections/oidc.rb +12 -56
  312. data/lib/hecks/projections/parser_table.rb +9 -28
  313. data/lib/hecks/projections/reference.rb +2 -15
  314. data/lib/hecks/projections/rust_vocabulary.rb +12 -250
  315. data/lib/hecks/projections/shape.rb +3 -29
  316. data/lib/hecks/projections/statements.rb +7 -86
  317. data/lib/hecks/projections/vocabulary.rb +6 -22
  318. data/lib/hecks/projections.rb +2 -16
  319. data/lib/hecks/projector/cli_projector.rb +60 -375
  320. data/lib/hecks/projector/docs_projector.rb +10 -201
  321. data/lib/hecks/projector/exporter.rb +42 -134
  322. data/lib/hecks/projector/ir_projector.rb +2 -9
  323. data/lib/hecks/projector/narrate_projector.rb +15 -189
  324. data/lib/hecks/projector/target.rb +13 -76
  325. data/lib/hecks/projector.rb +30 -121
  326. data/lib/hecks/query_specification/common/comparators.rb +5 -26
  327. data/lib/hecks/query_specification/common/comparison.rb +35 -143
  328. data/lib/hecks/query_specification/common/dsl.rb +8 -24
  329. data/lib/hecks/query_specification/common/null_policy.rb +20 -70
  330. data/lib/hecks/query_specification/common/options.rb +2 -6
  331. data/lib/hecks/query_specification/common/where_clause.rb +2 -10
  332. data/lib/hecks/query_specification/field_path.rb +37 -77
  333. data/lib/hecks/query_specification/hop_path.rb +12 -76
  334. data/lib/hecks/release/gem_pin.rb +2 -26
  335. data/lib/hecks/release/runner/ci_publisher.rb +3 -6
  336. data/lib/hecks/release/runner/commands.rb +2 -4
  337. data/lib/hecks/release/runner/gem_publisher.rb +2 -3
  338. data/lib/hecks/release/runner/npm_publisher.rb +6 -20
  339. data/lib/hecks/release/runner/preflight.rb +4 -19
  340. data/lib/hecks/release/runner/tagger.rb +2 -9
  341. data/lib/hecks/release/runner.rb +3 -32
  342. data/lib/hecks/rendering.rb +4 -26
  343. data/lib/hecks/router/namespace_installer.rb +12 -36
  344. data/lib/hecks/router.rb +4 -8
  345. data/lib/hecks/runtime/aggregate_lock.rb +8 -33
  346. data/lib/hecks/runtime/boot_gates.rb +3 -18
  347. data/lib/hecks/runtime/caller.rb +19 -59
  348. data/lib/hecks/runtime/capability_graph.rb +5 -21
  349. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +16 -78
  350. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +23 -160
  351. data/lib/hecks/runtime/command_interpreter.rb +124 -306
  352. data/lib/hecks/runtime/command_rules/admissibility.rb +95 -366
  353. data/lib/hecks/runtime/command_rules/arithmetic.rb +66 -196
  354. data/lib/hecks/runtime/command_rules/authorization.rb +13 -45
  355. data/lib/hecks/runtime/command_rules/emission.rb +8 -21
  356. data/lib/hecks/runtime/command_rules/references.rb +23 -202
  357. data/lib/hecks/runtime/dependency_planning.rb +30 -102
  358. data/lib/hecks/runtime/dispatcher.rb +82 -241
  359. data/lib/hecks/runtime/entity_element.rb +40 -462
  360. data/lib/hecks/runtime/entity_interpreter.rb +64 -219
  361. data/lib/hecks/runtime/errors.rb +32 -115
  362. data/lib/hecks/runtime/event.rb +3 -25
  363. data/lib/hecks/runtime/identity.rb +18 -154
  364. data/lib/hecks/runtime/instance.rb +18 -150
  365. data/lib/hecks/runtime/interpreting.rb +11 -70
  366. data/lib/hecks/runtime/invocation.rb +28 -164
  367. data/lib/hecks/runtime/loader.rb +48 -186
  368. data/lib/hecks/runtime/outbox.rb +38 -261
  369. data/lib/hecks/runtime/policy_interpreter.rb +38 -200
  370. data/lib/hecks/runtime/port_operation_interpreter.rb +26 -87
  371. data/lib/hecks/runtime/query_interpreter.rb +45 -187
  372. data/lib/hecks/runtime/reaction_invocation.rb +36 -138
  373. data/lib/hecks/runtime/read_model_interpreter.rb +117 -208
  374. data/lib/hecks/runtime/rebuild_sweep.rb +8 -54
  375. data/lib/hecks/runtime/reference_hop.rb +8 -81
  376. data/lib/hecks/runtime/refusal_wording.rb +8 -89
  377. data/lib/hecks/runtime/registry/saga_persistence.rb +10 -102
  378. data/lib/hecks/runtime/registry/verification.rb +52 -225
  379. data/lib/hecks/runtime/registry/world_defaults.rb +4 -53
  380. data/lib/hecks/runtime/registry.rb +99 -244
  381. data/lib/hecks/runtime/remote_dispatcher.rb +37 -152
  382. data/lib/hecks/runtime/routing.rb +4 -15
  383. data/lib/hecks/runtime/saga_interpreter/correlation.rb +14 -61
  384. data/lib/hecks/runtime/saga_interpreter.rb +79 -263
  385. data/lib/hecks/runtime/saga_pending_dispatch.rb +2 -43
  386. data/lib/hecks/runtime/tenant_check.rb +6 -68
  387. data/lib/hecks/runtime/tenant_scope.rb +3 -28
  388. data/lib/hecks/runtime/value/admission.rb +16 -67
  389. data/lib/hecks/runtime/value/coercion.rb +155 -641
  390. data/lib/hecks/runtime/value/entity_list_coercion.rb +26 -219
  391. data/lib/hecks/runtime/value.rb +12 -84
  392. data/lib/hecks/runtime.rb +17 -65
  393. data/lib/hecks/storehouse.rb +141 -568
  394. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +8 -32
  395. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +6 -23
  396. data/lib/hecks/vendoring/git_environment.rb +2 -8
  397. data/lib/hecks/vendoring/git_source.rb +4 -10
  398. data/lib/hecks/vendoring.rb +7 -46
  399. data/lib/hecks/version.rb +3 -14
  400. data/lib/hecks.rb +26 -81
  401. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -46
  402. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +11 -80
  403. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +10 -99
  404. metadata +3 -2
@@ -8,46 +8,25 @@ require_relative "invariant_violation"
8
8
  module Hecks
9
9
  module Runtime
10
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.
11
+ # Class-side coercion engine for Value, extended in so its methods read
12
+ # as `Value.for`, `Value.build`, and so on.
14
13
  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.
14
+ # The complete set of attribute value shapes. Mirrored by hand into a
15
+ # generated Rust enum (bin/project_kernel_capabilities) — adding a shape
16
+ # here without a matching Rust file leaves the kernel unaware of it.
35
17
  SHAPES = %i[scalar list optional composite].freeze
36
18
 
37
19
  # Coerces `value` for one of `aggregate`'s declared attributes, by name.
38
20
  #
39
- # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct
40
- # `name` is looked up on
21
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] construct to look
22
+ # up `name` on
41
23
  # @param name [String, Symbol] the declared attribute name
42
24
  # @param value [Object] the raw value to coerce
43
- # @return [Runtime::Value, Object, nil] `value` unchanged when `aggregate`
44
- # declares no such attribute; otherwise `for_attribute`'s own result
45
- # @raise [Runtime::TypeMismatch] if `value` cannot be coerced to the
46
- # attribute's declared type
47
- # @raise [Runtime::UnknownArgument] if `value` is a Hash naming a field the
48
- # attribute's own value-object type does not declare
49
- # @raise [Runtime::InvariantViolation] if a coerced value object breaks one
50
- # of its own invariants
25
+ # @return [Runtime::Value, Object, nil] the coerced value, or `value`
26
+ # unchanged if `aggregate` declares no such attribute
27
+ # @raise [Runtime::TypeMismatch] if `value` cannot be coerced to the declared type
28
+ # @raise [Runtime::UnknownArgument] if `value` is a Hash naming an undeclared field
29
+ # @raise [Runtime::InvariantViolation] if a coerced value object breaks an invariant
51
30
  def for(aggregate, name, value)
52
31
  attribute = aggregate.attribute(name)
53
32
  return value unless attribute
@@ -55,60 +34,18 @@ module Hecks
55
34
  for_attribute(aggregate, attribute, value)
56
35
  end
57
36
 
58
- # The four `SHAPES` above, in the order this method actually checks
59
- # them: `optional`/nil-passthrough first (a value that isn't there
60
- # has no shape left to branch on), `list` second (a list of
61
- # elements, hydrated as entities), then — inside `coerced =` —
62
- # `composite` (the type names a declared value object, rebuilt
63
- # recursively via `build`) with `scalar` as what's left once
64
- # neither of those applies (the raw value, passed through
65
- # unchanged).
66
- # `boundary: false` is the query door (`QueryInterpreter#normalize_args`):
67
- # a query attribute's declared type names the argument for callers and
68
- # generators, never a runtime shape — comparison unwraps both sides
69
- # itself, so a `reference: {value: ...}` offered against a `String`
70
- # query field is the documented allowance (see banking's own
71
- # `Account.OpenForCustomer`), not a C3.8 mismatch.
72
- # `argument: true` is the command/entity/port argument door only
73
- # (`Interpreting#normalize_args`, every command/entity/port
74
- # dispatch): the one place a nil for a non-optional attribute is
75
- # the caller leaving a required argument empty (C3.7), absorbed
76
- # via the type's own field defaults when every field has one
77
- # (`nil_argument` below). Every other caller — `sets` copying an
78
- # optional argument into state, hydration, entity elements,
79
- # identity, defaults — is state assembly, where nil is a
80
- # legitimate "absent is not empty" value the aggregate's own
81
- # attribute may hold. `QueryInterpreter#normalize_args` never
82
- # passes `argument: true` — a null required value-object-typed
83
- # query argument is checked, and refused, entirely on its own
84
- # side (`null_vo_argument!`, query_interpreter.rb) precisely so
85
- # it does not reach this default-absorbing fallback (QualityControl
86
- # BUG#36 — a query's own null VO argument must refuse regardless
87
- # of any default, unlike a command's).
37
+ # Coerces `value` for a single, already-resolved `attribute`, branching on
38
+ # its declared shape (list, reference, composite, or bare scalar).
88
39
  #
89
- # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct
90
- # `attribute` is declared on
91
- # @param attribute [Bluebook::Attribute, nil] the attribute to coerce `value`
92
- # for; nil is treated as an unknown attribute (no shape to coerce against)
93
- # @param value [Object] the raw value to coerce
94
- # @param boundary [Boolean] whether a bare-primitive attribute's own scalar
95
- # shape is checked; false at the query door, where a declared type names
96
- # the argument for callers/generators only
97
- # @param argument [Boolean] whether this is the command/entity/port argument
98
- # door, where a nil for a non-optional attribute is a left-empty argument
99
- # (C3.7) rather than ordinary state-assembly nil
100
- # @return [Runtime::Value, Object, nil] the coerced value: a built `Value`
101
- # for a composite-typed attribute, a frozen Array for a `has_many`
102
- # reference or hydrated entity list, a joined identity String for a
103
- # scalar reference, or `value` passed through unchanged for a bare
104
- # primitive or an unknown attribute; nil for an absent optional attribute
105
- # @raise [Runtime::TypeMismatch] if `value` cannot be coerced to the
106
- # attribute's declared type, or a required reference/attribute is offered
107
- # as a wrong-shaped value
108
- # @raise [Runtime::UnknownArgument] if `value` is a Hash naming a field the
109
- # attribute's own value-object type does not declare
110
- # @raise [Runtime::InvariantViolation] if a coerced value object breaks one
111
- # of its own invariants
40
+ # `boundary: false` is the query door, where a declared type documents the
41
+ # argument for callers/generators rather than naming a shape to enforce.
42
+ # `argument: true` is the command/entity/port dispatch door, where a nil for
43
+ # a required attribute is a left-empty argument (C3.7), not ordinary state nil.
44
+ #
45
+ # @raise [Runtime::TypeMismatch] if `value` cannot be coerced, or a required
46
+ # reference/attribute is a wrong-shaped value
47
+ # @raise [Runtime::UnknownArgument] if `value` is a Hash naming an undeclared field
48
+ # @raise [Runtime::InvariantViolation] if a coerced value object breaks an invariant
112
49
  def for_attribute(aggregate, attribute, value, boundary: true, argument: false)
113
50
  return nil_or_missing(aggregate, attribute, value, argument) if attribute.nil? || value.nil?
114
51
  return reference_list(attribute, value) if attribute.list? && attribute.reference?
@@ -116,14 +53,9 @@ module Hecks
116
53
  return hydrate_entity_list(aggregate, attribute, value) if attribute.list? # :list
117
54
  return value unless aggregate.respond_to?(:value_object)
118
55
 
119
- # The set the attribute names is checked where the attribute is known.
120
- # `build` below sees only the value object, never which attribute asked
121
- # for it, so a command argument's `admits:` has to be read here — this
122
- # is the door every argument and every head field comes through.
123
- #
124
- # After coercion, not before: a scalar arrives wrapped in whatever holder
125
- # its type names (`{value: "append"}` for an OpName), and checking the
126
- # raw payload would be checking the envelope.
56
+ # `admits:` is checked here, where the attribute is known — `build` only
57
+ # sees the value object. Checked after coercion: a scalar arrives wrapped
58
+ # in its type's own holder, and checking the raw payload would be wrong.
127
59
  value_object = value_object_for(aggregate, attribute.type)
128
60
  return bare_primitive(aggregate, attribute, value, boundary) if value_object.nil?
129
61
 
@@ -137,22 +69,9 @@ module Hecks
137
69
  coerced
138
70
  end
139
71
 
140
- # NIL is not a value for a non-optional argument (C3.7/C3.8). An
141
- # `optional:` attribute and a load from the store pass nil through
142
- # as they always did; a command argument offered as null for a
143
- # required attribute is
144
- # refused as the field the caller left empty — a value object is
145
- # built from no fields, so its first required field refuses with
146
- # exactly the wording the Rust side's `from_json` gives it
147
- # ("Money.cents expects Integer, got nil"), and a bare scalar
148
- # refuses through `check_bare_primitive`'s own wording. Lists and
149
- # references keep their nil passthrough (their absence is an empty
150
- # relationship, `validate_relationship_cardinality`'s business).
151
- # The `attribute.nil?`/`value.nil?` branch of `for_attribute`,
152
- # pulled out on its own — an unknown attribute has no shape left
153
- # to branch on, and a nil value is either an ordinary absence
154
- # (state assembly, hydration, a query ask) or, at the argument
155
- # door only, `nil_argument`'s own C3.7 refusal.
72
+ # A required argument's nil is refused (C3.7); state assembly, hydration
73
+ # and query nils pass through unchanged. Lists/references keep their own
74
+ # nil passthrough regardless of `argument`.
156
75
  private def nil_or_missing(aggregate, attribute, value, argument)
157
76
  return value if attribute.nil? || !value.nil?
158
77
 
@@ -172,31 +91,21 @@ module Hecks
172
91
  expected: attribute.type, offered: "nil")
173
92
  end
174
93
 
175
- # A bare primitive is type-checked at the boundary too (C3.8,
176
- # docs/semantics/bluebook-semantics.md) — the same two predicates a
177
- # value object's own fields get, so wrong-typed caller input is a
178
- # TypeMismatch refusal here, never an evaluation fault later. Its
179
- # `admits:` set is still checked, exactly as a value object's is.
94
+ # A bare primitive is boundary-checked too (C3.8) — wrong-typed input
95
+ # refuses here as a TypeMismatch, never later as a broken predicate.
96
+ # Its `admits:` set is still checked, exactly as a value object's is.
180
97
  private def bare_primitive(aggregate, attribute, value, boundary)
181
98
  check_bare_primitive(aggregate, attribute, value) if boundary
182
99
  admit_declared_set(aggregate, attribute, value)
183
100
  value
184
101
  end
185
102
 
186
- # Resolves the value-object class `type` names, searching `aggregate`'s own
187
- # declarations first, then its chapter's other aggregates.
188
- #
189
- # Aggregate-local value objects remain authoritative, which permits
190
- # intentional duplication. An ordinary fact may also name an identity
191
- # value object declared on another aggregate; that shape is borrowed
192
- # only when every chapter declaration with the name agrees.
103
+ # Resolves the value-object class `type` names: `aggregate`'s own
104
+ # declarations first, then its chapter's other aggregates if they agree.
193
105
  #
194
- # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct to
195
- # search first
106
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] construct to search first
196
107
  # @param type [String, Symbol, #to_s] the declared type name to resolve
197
- # @return [Class, nil] the `Bluebook::ValueObject` subclass `type` names; nil
198
- # if `aggregate` declares none, and its chapter's other aggregates disagree
199
- # on the shape of every same-named one (or declare none either)
108
+ # @return [Class, nil] the `Bluebook::ValueObject` subclass `type` names, or nil
200
109
  def value_object_for(aggregate, type)
201
110
  local = aggregate.value_object(type)
202
111
  return local if local
@@ -214,18 +123,11 @@ module Hecks
214
123
  # Coerces a reference-typed attribute's offered value into the target's own
215
124
  # canonical identity string.
216
125
  #
217
- # Retained relationships store canonical target identities, not Ruby
218
- # Value wrappers. Raw scalar IDs remain a compatibility input. A named
219
- # identity VO omits its minted aggregate field at the command boundary;
220
- # a bespoke compound VO may instead name the target heads directly.
221
- # Neither form requires reverse-splitting a canonical ID.
222
- #
223
126
  # @param attribute [Bluebook::Attribute] the reference-typed attribute
224
- # @param value [Object] the offered value: a bare scalar identity, a
225
- # `Runtime::Value`, or a Hash naming the target's own identity fields
226
- # @return [String, Object] the joined canonical identity String once every
227
- # identity part resolves; `value` unchanged otherwise (a bare scalar, an
228
- # unresolvable target, or a shape `sole_scalar_identity` cannot unwrap)
127
+ # @param value [Object] a bare scalar identity, a `Runtime::Value`, or a Hash
128
+ # naming the target's own identity fields
129
+ # @return [String, Object] the joined identity String, or `value` unchanged
130
+ # when it cannot be resolved
229
131
  def reference_identity(attribute, value)
230
132
  return value unless value.is_a?(self) || value.is_a?(Hash)
231
133
 
@@ -245,42 +147,13 @@ module Hecks
245
147
  Naming.identity(parts)
246
148
  end
247
149
 
248
- # A command may redeclare a `reference_to` field under its own,
249
- # differently-named single-attribute value object (`attribute
250
- # :venue, VenueHandle; sets :venue`, not `reference_to Venue` —
251
- # QualityControl BUG#121, `qa/stress_domains/generated_revalued_
252
- # shape`'s own `Hangar.Repoint`) instead of naming the target's
253
- # own identity field(s) directly. `identity_part`'s path walk
254
- # above only ever matches an incoming shape that already uses the
255
- # target's own field names (or is the target's own identity value
256
- # object, `direct_identity_head`) — an ad hoc wrapper around a
257
- # bare scalar under some other field name (`VenueHandle`'s own
258
- # `value`, not `Venue`'s own `code`) fails every path lookup, and
259
- # without this would fall through to the unresolved `return value`
260
- # above, storing the wrapped Value. That leaves one reference field on
261
- # one aggregate holding two different shapes depending on which
262
- # command last wrote it — `Open`'s own bare `reference_to Venue`
263
- # argument was never wrapped in the first place (the top guard
264
- # clause passes a bare scalar straight through), so it always
265
- # stored the canonical bare identity this class's own header
266
- # comment promises ("canonical target identities, not Ruby Value
267
- # wrappers") — while `Repoint` silently kept the wrapper instead.
268
- #
269
- # Unambiguous only when both sides admit exactly one scalar: the
270
- # target names exactly one identity path (`paths.one?` — a
271
- # compound identity has no single field either side could stand
272
- # in for) and the offered value is itself a single-attribute
273
- # value object (`sole_attribute` — a multi-field VO has no one
274
- # scalar to unwrap either). `materialize_unwrapped` recurses
275
- # through any further single-field wrapping the same way it
276
- # already does for `Value.materialize_unwrapped`'s other callers,
277
- # landing on the bare scalar `Open`'s own path already produces
278
- # for the identical target field.
150
+ # Unwraps `value` to a bare scalar when `paths` names exactly one identity
151
+ # field and `value` is itself a single-attribute value object — the only
152
+ # case where unwrapping cannot be ambiguous.
279
153
  #
280
154
  # @param value [Object] the offered reference value to unwrap
281
155
  # @param paths [Array<String>] the target's own declared identity paths
282
- # @return [Object, nil] the unwrapped scalar when `value` is a single-field
283
- # value object and `paths` names exactly one path; nil otherwise
156
+ # @return [Object, nil] the unwrapped scalar, or nil if it does not apply
284
157
  def sole_scalar_identity(value, paths)
285
158
  return nil unless value.is_a?(self) && paths.one?
286
159
  return nil unless value.value_object.sole_attribute
@@ -288,14 +161,11 @@ module Hecks
288
161
  materialize_unwrapped(value)
289
162
  end
290
163
 
291
- # Whether `value` is itself the target's own (single) identity
292
- # value object — pure, self-contained: reads only `value` and
293
- # `target`, decides nothing about any particular path.
164
+ # Whether `value` is itself the target's own single identity value object.
294
165
  #
295
166
  # @param value [Object] the offered reference value to check
296
167
  # @param target [Bluebook::Aggregate] the reference's own resolved target
297
- # @return [String, nil] the target's own single identity head, as a String,
298
- # when `value` is that head's own declared value-object type; nil otherwise
168
+ # @return [String, nil] the target's identity head name, or nil
299
169
  def direct_identity_head(value, target)
300
170
  return nil unless value.is_a?(self) && target.identity_heads.one?
301
171
 
@@ -303,29 +173,21 @@ module Hecks
303
173
  target.attribute(head)&.type.to_s == value.type_name ? head.to_s : nil
304
174
  end
305
175
 
306
- # One identity path's own value out of the materialized hash —
307
- # stripping a leading segment already covered by
308
- # `direct_identity_head`, then walking the rest. Pure given its
309
- # three inputs; extracted from `reference_identity` alongside
310
- # `direct_identity_head` above purely to keep that method to its
311
- # own guard-clause shape.
176
+ # One identity path's own value out of the materialized hash, stripping a
177
+ # leading segment already covered by `direct_identity_head`.
312
178
  #
313
- # @param materialized [Hash, Object] the offered reference value, already
314
- # materialized to a plain Hash (or scalar, for a path that finds nothing)
179
+ # @param materialized [Hash, Object] the offered reference value as a Hash
315
180
  # @param path [String, Symbol] one dotted identity path to dig
316
- # @param direct_head [String, nil] the leading segment to strip, when `path`
317
- # restates the head `direct_identity_head` already matched
318
- # @return [Object, nil] the value found by walking `path`'s segments; nil if
319
- # any segment is missing or a non-Hash is dug into before the path ends
181
+ # @param direct_head [String, nil] leading segment to strip, if present
182
+ # @return [Object, nil] the value found by walking `path`, or nil if missing
320
183
  def identity_part(materialized, path, direct_head)
321
184
  segments = path.to_s.split(".")
322
185
  segments.shift if direct_head && segments.first == direct_head
323
186
  segments.reduce(materialized) do |held, segment|
324
187
  next nil unless held.is_a?(Hash)
325
188
 
326
- # `key?` decides which spelling answers, never `||` — a
327
- # genuinely-held `false` must not fall through to the
328
- # other spelling (usually absent) and read as `nil`.
189
+ # `key?` decides which spelling answers — a genuinely-held `false`
190
+ # must not fall through to the other spelling and read as `nil`.
329
191
  sym = segment.to_sym
330
192
  held.key?(sym) ? held[sym] : held[segment]
331
193
  end
@@ -350,40 +212,21 @@ module Hecks
350
212
  # defaults, nested normalization and validation run.
351
213
  #
352
214
  # @param value_object [Class] the target `Bluebook::ValueObject` subclass
353
- # @param name [String, Symbol] the attribute or argument name, quoted in a
354
- # refusal
355
- # @param value [Hash, Runtime::Value, Object] the offered value: a Hash of
356
- # fields, an already-built `Value` of a differently-named but same-shaped
357
- # type, or a bare scalar for a single-field value object
215
+ # @param name [String, Symbol] the attribute or argument name, quoted in a refusal
216
+ # @param value [Hash, Runtime::Value, Object] a Hash of fields, an already-built
217
+ # `Value`, or a bare scalar for a single-field value object
358
218
  # @return [Hash{Symbol => Object}] the offered fields, keyed by attribute name
359
219
  # @raise [Runtime::TypeMismatch] if `value` is a bare scalar and `value_object`
360
220
  # declares more than one field
361
221
  def fields_for(value_object, name, value)
362
222
  return value.transform_keys(&:to_sym) if value.is_a?(Hash)
363
- # Mutations may legitimately carry a value object into a differently
364
- # named value-object slot with the same declared fields (for example,
365
- # PositiveMoney into an Account's Money balance). Rebuild the target
366
- # type from its state; callers at the public boundary still have to
367
- # supply an object rather than a scalar.
223
+ # A same-shaped value object may fill a differently-named slot (e.g.
224
+ # PositiveMoney into an Account's Money balance) — rebuild from its state.
368
225
  return value.to_h if value.is_a?(self)
369
226
 
370
- # Vendored addition, not (yet) upstream hecks (migration
371
- # plan task 5): a bare scalar auto-wraps into a single-field
372
- # value object's sole attribute -- the same shape
373
- # #from_identifier already establishes for identity coercion
374
- # (`build(value_object, { fields.first.name => identifier }) if
375
- # fields.size == 1`), made consistent here for mutation
376
- # coercion too. Real, corpus-wide gap: a synthesised single-
377
- # field wrapper (Part 3a's bare-primitive auto-synthesis, the
378
- # norm for a VO-typed aggregate field) is exactly the shape
379
- # #rewrap_arithmetic_result hands back a raw scalar result to
380
- # -- without this, every phantom-field increment/multiply on a
381
- # single-field-wrapped attribute refused with "pass its fields
382
- # as an object, not <scalar>" the instant it tried to re-wrap
383
- # its own correctly-computed result. Multi-field VOs still
384
- # refuse below, unchanged -- only the genuinely unambiguous
385
- # single-field case auto-wraps, matching from_identifier's own
386
- # precedent exactly.
227
+ # A bare scalar auto-wraps into a single-field value object's sole
228
+ # attribute, matching `from_identifier`'s own precedent. Multi-field
229
+ # value objects still refuse below.
387
230
  return { value_object.attributes.first.name => value } if value_object.attributes.size == 1
388
231
 
389
232
  raise TypeMismatch,
@@ -392,76 +235,27 @@ module Hecks
392
235
  offered: Rendering.describe(value))
393
236
  end
394
237
 
395
- # `build`'s own recursive twin of `for_attribute`'s single-level
396
- # normalization — a value object's own composite-typed fields
397
- # (`Pizza.price_cents`, a `Price`) never otherwise pass back
398
- # through `fields_for`, so a bare scalar or partial Hash for one
399
- # of those sails past the outer VO's own shape check (`Pizza`
400
- # itself has two fields, so nothing unwraps there) and lands
401
- # stored one field down exactly as handed in — found live: once
402
- # the fuzzer actually generated the bare-scalar shape
403
- # `fields_for` has accepted at the top level since 86727afd, a
404
- # nested `Price` stored as a raw Integer broke every later
405
- # dotted-path read (`pizza.price_cents.cents`) expecting one
406
- # more level of Hash.
238
+ # `build`'s own recursive twin of `for_attribute`, normalizing a value
239
+ # object's own composite-typed fields into their declared shape.
407
240
  #
408
- # Stays a plain Hash, never a nested `Value` — `Value#with`'s own
409
- # header and `materialize_unwrapped`'s comment already depend on
410
- # a value-object-typed field of another value object staying a
411
- # plain Hash once stored, and this does not change that; it only
412
- # makes sure that Hash has the shape its own type declares.
413
- # `aggregate` is the one thing this needs beyond `fields_for`'s own
414
- # scope — a nested type can only be resolved through `aggregate.
415
- # value_object(name)`, so callers with no aggregate in reach
416
- # (`Value#with`, always re-setting an already-scalar arithmetic
417
- # field) simply skip this and keep their own behavior unchanged.
418
- # Recurses into each nested field's own validation too, not only its
419
- # shape — found live alongside the shape bug this method's header
420
- # already describes: a nested `Price`/`Size` (a value-object-typed
421
- # field of another value object, e.g. `Pizza.price_cents`,
422
- # `Pizza.size`) had its Hash shape normalized here but never ran
423
- # `validate!` — `build`, below, only ever validated the outer value
424
- # object's own direct fields, so a negative `price_cents.cents` or an
425
- # out-of-`one_of` `size.value` sailed through a `Pizza`-typed command
426
- # argument untouched, while the exact same nested type declared as a
427
- # direct, top-level command attribute (`SafeDepositBox.Rent`'s own
428
- # `attribute :size, Size`) was already checked correctly. `apply_
429
- # defaults` runs first, same as the outer value object gets in
430
- # `build`, so a nested field's own default is filled in before its
431
- # own invariants read it.
241
+ # Stays a plain Hash, never a nested `Value` — `Value#with` depends on that.
432
242
  #
433
- # @param aggregate [Bluebook::Aggregate, Bluebook::Entity, nil] the construct
434
- # nested types are resolved against; a no-op if it does not respond to
435
- # `value_object`
436
- # @param value_object [Class] the `Bluebook::ValueObject` subclass `fields`
437
- # belongs to
438
- # @param fields [Hash{Symbol => Object}] the outer value object's own fields,
439
- # already defaulted; written in place
440
- # @return [Hash{Symbol => Object}] `fields`, with every composite-typed field
441
- # normalized (and, for a non-list one, validated) into its own declared shape
442
- # @raise [Runtime::TypeMismatch] if a nested field cannot be coerced to its
443
- # declared type
444
- # @raise [Runtime::UnknownArgument] if a nested field's own Hash names a
445
- # field its declared type does not declare
446
- # @raise [Runtime::InvariantViolation] if a nested field breaks one of its
447
- # own invariants
243
+ # @param aggregate [Bluebook::Aggregate, Entity, nil] nested-type lookup scope
244
+ # @param value_object [Class] the value object `fields` belongs to
245
+ # @param fields [Hash{Symbol => Object}] the outer fields, already defaulted
246
+ # @return [Hash{Symbol => Object}] `fields` with composite fields normalized
247
+ # @raise [Runtime::TypeMismatch] if a nested field cannot be coerced
248
+ # @raise [Runtime::UnknownArgument] if a nested field names an undeclared key
249
+ # @raise [Runtime::InvariantViolation] if a nested field breaks its own invariant
448
250
  def normalize_composite_fields(aggregate, value_object, fields)
449
251
  return fields unless aggregate.respond_to?(:value_object)
450
252
 
451
253
  value_object.attributes.each do |attribute|
452
254
  next unless fields.key?(attribute.name)
453
255
 
454
- # A list member read back from the store hydrates like a top-level
455
- # list — `list_of(Entity)` elements get their fields coerced,
456
- # `list_of(ValueObject)` elements become Values — so a value object
457
- # holding a list loads into the same shape the live dispatch that
458
- # wrote it held. Found by PR A3 (every adapter through the state
459
- # codec): chess-style `sets :positions, append: { pieces:
460
- # state(:pieces) }` snapshots read back from Heki/Sqlite/Postgres
461
- # (and now Memory's codec copy) with raw element hashes, so a
462
- # `given` comparing a snapshot piece's `id` Value to a live one
463
- # never matched after a restart. Load door only: an input list
464
- # member is left exactly as before, refusals unchanged.
256
+ # A list member hydrates the same as a top-level list, so a value
257
+ # read back from the store matches the shape a live dispatch wrote.
258
+ # Load door only — an input list member is left as offered.
465
259
  if attribute.list?
466
260
  fields[attribute.name] = for_attribute(aggregate, attribute, fields[attribute.name]) if trusting_stored_state?
467
261
  next
@@ -482,13 +276,12 @@ module Hecks
482
276
  fields
483
277
  end
484
278
 
485
- # Fills every declared attribute `fields` does not already hold with its
486
- # own declared `default:`, when it has one.
279
+ # Fills every declared attribute `fields` lacks with its own `default:`.
487
280
  #
488
281
  # @param value_object [Class] the `Bluebook::ValueObject` subclass whose
489
282
  # declared defaults are read
490
283
  # @param fields [Hash{Symbol => Object}] the offered fields; written in place
491
- # @return [Hash{Symbol => Object}] `fields`, with each declared default filled in
284
+ # @return [Hash{Symbol => Object}] `fields`, with defaults filled in
492
285
  def apply_defaults(value_object, fields)
493
286
  value_object.attributes.each_with_object(fields) do |attribute, completed|
494
287
  completed[attribute.name] = attribute.default unless completed.key?(attribute.name) || attribute.default.nil?
@@ -496,30 +289,20 @@ module Hecks
496
289
  end
497
290
 
498
291
  # The full door a value object's own fields pass through — shared by
499
- # `build` (the outer value object) and `normalize_composite_fields`
500
- # (every nested one), so a nested `Price`/`Size` is refused exactly
501
- # the same way, with exactly the same wording, as the identical type
502
- # declared directly on a command.
292
+ # `build` and `normalize_composite_fields`, so a nested field refuses
293
+ # exactly like the same type declared directly on a command.
503
294
  #
504
295
  # @param value_object [Class] the `Bluebook::ValueObject` subclass to
505
296
  # validate `fields` against
506
- # @param fields [Hash{Symbol => Object}] the already-defaulted, already
507
- # nested-normalized fields to check
297
+ # @param fields [Hash{Symbol => Object}] already-defaulted, nested-normalized fields
508
298
  # @return [void]
509
- # @raise [Runtime::UnknownArgument] if `fields` names a key `value_object`
510
- # does not declare
511
- # @raise [Runtime::TypeMismatch] if a required field is missing, a numeric
512
- # or pattern-constrained field is the wrong shape, or a scalar field
513
- # arrives as a composite
514
- # @raise [Runtime::InvariantViolation] if `fields` breaks one of
515
- # `value_object`'s own declared invariants
299
+ # @raise [Runtime::UnknownArgument] if `fields` names an undeclared key
300
+ # @raise [Runtime::TypeMismatch] if a required field is missing or wrong-shaped
301
+ # @raise [Runtime::InvariantViolation] if `fields` breaks a declared invariant
516
302
  def validate!(value_object, fields)
517
- # C6.3 (docs/semantics/bluebook-semantics.md) — a value object is
518
- # validated on construction from input only; state read back from
519
- # the store is trusted as it was written, so tightening an
520
- # invariant never makes an old record unreadable (migration is
521
- # the era system's job). `hydrate` — the one load door — sets
522
- # the flag; every input door leaves it unset.
303
+ # C6.3 — a value object validates on construction from input only; state
304
+ # read back from the store is trusted as written, so tightening an
305
+ # invariant never makes an old record unreadable. `hydrate` sets this flag.
523
306
  return if trusting_stored_state?
524
307
 
525
308
  check_unknown_fields(value_object, fields)
@@ -545,16 +328,12 @@ module Hecks
545
328
  # @param value_object [Class] the `Bluebook::ValueObject` subclass to build
546
329
  # @param fields [Hash{Symbol, String => Object}] the offered field values,
547
330
  # either key spelling
548
- # @param aggregate [Bluebook::Aggregate, Bluebook::Entity, nil] the construct
549
- # a nested composite field is resolved against; nil skips nested
550
- # normalization entirely
331
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity, nil] construct a
332
+ # nested composite field resolves against; nil skips nested normalization
551
333
  # @return [Runtime::Value] the built, validated value object
552
- # @raise [Runtime::UnknownArgument] if `fields` (or a nested field) names a
553
- # key its own type does not declare
554
- # @raise [Runtime::TypeMismatch] if a field (or a nested one) cannot be
555
- # coerced to its declared type
556
- # @raise [Runtime::InvariantViolation] if the built value object (or a
557
- # nested one) breaks one of its own invariants
334
+ # @raise [Runtime::UnknownArgument] if a field names an undeclared key
335
+ # @raise [Runtime::TypeMismatch] if a field cannot be coerced to its declared type
336
+ # @raise [Runtime::InvariantViolation] if the built value object breaks an invariant
558
337
  def build(value_object, fields, aggregate = nil)
559
338
  fields = apply_defaults(value_object, fields.transform_keys(&:to_sym))
560
339
  fields = normalize_composite_fields(aggregate, value_object, fields)
@@ -562,22 +341,15 @@ module Hecks
562
341
  new(value_object, fields)
563
342
  end
564
343
 
565
- # State arrives decoded or not at all (Phase 2, Track A, PR A4).
566
- # Every persistence adapter reads through `Ports::Persistence::
567
- # StateCodec.decode` (A3), which symbolizes every top-level key, and
568
- # the runtime's own callers (entity elements, the remote dispatcher's
569
- # `symbolize_names:` parse, Era's audit) build symbol-keyed state
570
- # themselves. A String key here is an adapter or caller that skipped
571
- # the codec, so it is refused by name rather than respelled: a silent
572
- # `to_sym` here would hide exactly this kind of bypass instead. Always
573
- # on, because it costs one `is_a?` per key, no more than a `to_sym` would.
344
+ # State arrives decoded or not at all: every persistence adapter symbolizes
345
+ # keys before this point, so a String key here means a caller skipped that
346
+ # step — refused by name rather than silently respelled.
574
347
  #
575
- # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct
576
- # whose declared attributes coerce `state`'s own values
348
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] construct whose
349
+ # declared attributes coerce `state`'s own values
577
350
  # @param state [Hash{Symbol => Object}] the stored state to hydrate; every
578
351
  # key must already be a Symbol
579
- # @return [Hash{Symbol => Object}] `state`, coerced through every declared
580
- # attribute it names
352
+ # @return [Hash{Symbol => Object}] `state`, coerced through each declared attribute
581
353
  # @raise [Runtime::WiringError] if `state` holds any non-Symbol key
582
354
  def hydrate(aggregate, state)
583
355
  undecoded = state.keys.grep_v(Symbol)
@@ -600,9 +372,8 @@ module Hecks
600
372
  # Marks the block as loading trusted, already-validated stored state, so
601
373
  # `validate!` skips its own checks for the block's duration.
602
374
  #
603
- # `Thread.current`-backed, not a plain ivar, so two threads hydrating
604
- # concurrently on the same `Value` singleton class never see or clear
605
- # each other's flag.
375
+ # Thread-local, not a plain ivar, so two threads hydrating concurrently
376
+ # never see or clear each other's flag.
606
377
  #
607
378
  # @yield the code that should see `trusting_stored_state?` true
608
379
  # @return [Object] the block's result
@@ -614,59 +385,27 @@ module Hecks
614
385
  Thread.current[TRUSTED_LOAD_KEY] = previous
615
386
  end
616
387
 
617
- # Reports whether the current thread is inside a `trusting_stored_state` block.
388
+ # Whether the current thread is inside a `trusting_stored_state` block.
618
389
  #
619
- # @return [Boolean] true if a `trusting_stored_state` block is on this
620
- # thread's own call stack
390
+ # @return [Boolean] true if on this thread's own call stack
621
391
  def trusting_stored_state? = Thread.current[TRUSTED_LOAD_KEY] == true
622
392
 
623
- # QualityControl BUG#125 — the one narrow door `check_scalar_shapes`
624
- # keeps open, now that a non-string scalar is otherwise refused for a
625
- # String-typed field. `MetaValidator::Judge#send_to` — the single
626
- # choke point every one of the language's own self-hosted dispatches
627
- # goes through while walking a bluebook's declarations into the
628
- # "Bluebook" meta-domain — wraps itself in this, and nothing else
629
- # does. `Judge#appends`' generic `POSITION` handling
630
- # (judge.rb#appends) keys purely off a field being named "position",
631
- # the convention every other append list actually uses it for
632
- # (ValueObject::Member, ProcessManager::Handler, ... — all really
633
- # `Position`/Integer-typed); `Normalise`'s own `NormalisationRule`
634
- # happens to also name its own domain field "position"
635
- # (bluebook.bluebook), but declares it `RuleText` (String) — so the
636
- # same walk-index substitution (`Judge#v(index)`) hands it a raw
637
- # Integer too, on every domain's very first boot (the language
638
- # self-judges its own grammar via `MetaValidator.fresh_runtime`'s
639
- # fixpoint). Confirmed (QualityControl BUG#125 investigation): the
640
- # resulting value is never read back — `normalisations` is an
641
- # `ELSEWHERE`/`derived` field spliced straight from
642
- # `Expression::CanonicalForm.table` (assembly/contracts.rb), so the
643
- # judged record holding the Integer is discarded whole — this is a
644
- # walk-index/domain-field name collision inside `Judge#appends`, not
645
- # a genuine semantic need for `position` to arrive numeric. Fixing
646
- # that collision at its own root is a separate, larger change to
647
- # self-hosted bootstrap mechanics that every domain's boot depends
648
- # on; this flag only ever loosens scalar-shape checking for the
649
- # meta-grammar's own value objects (RuleText, BluebookName, Position,
650
- # …) that Judge itself constructs while walking a bluebook's
651
- # declarations — never for a real domain's own declared value
652
- # objects (PieceId, Money, …), which Judge never dispatches commands
653
- # against. `offer` (judge.rb) already converts a `TypeMismatch` here
654
- # into a recorded refusal rather than letting it propagate, but
655
- # `MetaValidator.call` raises the instant `refusals` is non-empty
656
- # (meta_validator.rb) — so, unexempted, this would fail every
657
- # domain's boot, not just the language's own bootstrap. Composite
658
- # shapes (Array/Hash) stay refused unconditionally, bootstrap or not
659
- # — nothing Judge does ever legitimately needs those for a scalar
660
- # field.
393
+ # `MetaValidator::Judge#send_to` walks a bluebook's own declarations through
394
+ # the self-hosted "Bluebook" meta-domain, and its generic append handling
395
+ # keys off a field's name ("position") rather than its declared type — so
396
+ # the language's own grammar can hand this a raw Integer for a String-typed
397
+ # meta field on every domain's first boot. This flag loosens
398
+ # `check_scalar_shapes`'s String check only for that one caller; composite
399
+ # shapes (Array/Hash) stay refused unconditionally, bootstrap or not, and no
400
+ # real domain's own declared value objects are affected.
661
401
  BOOTSTRAP_KEY = :hecks_judge_bootstrapping
662
402
 
663
- # Marks the block as `MetaValidator::Judge#send_to`'s own self-hosted
664
- # bootstrap dispatch, so `check_scalar_shapes` loosens its `String` check
665
- # for the block's duration.
403
+ # Marks the block as `MetaValidator::Judge#send_to`'s self-hosted bootstrap
404
+ # dispatch, so `check_scalar_shapes` loosens its `String` check for the
405
+ # block's duration.
666
406
  #
667
- # `Thread.current`-backed, not a plain ivar, so two threads bootstrapping
668
- # concurrently on the same `Value` singleton class never see or clear
669
- # each other's flag.
407
+ # Thread-local, not a plain ivar, so two threads bootstrapping concurrently
408
+ # never see or clear each other's flag.
670
409
  #
671
410
  # @yield the code that should see `judge_bootstrapping?` true
672
411
  # @return [Object] the block's result
@@ -678,83 +417,22 @@ module Hecks
678
417
  Thread.current[BOOTSTRAP_KEY] = previous
679
418
  end
680
419
 
681
- # Reports whether the current thread is inside a `judge_bootstrapping` block.
420
+ # Whether the current thread is inside a `judge_bootstrapping` block.
682
421
  #
683
- # @return [Boolean] true if a `judge_bootstrapping` block is on this
684
- # thread's own call stack
422
+ # @return [Boolean] true if on this thread's own call stack
685
423
  def judge_bootstrapping? = Thread.current[BOOTSTRAP_KEY] == true
686
424
 
687
- # An identity names its field — `identified_by :number` — and the
688
- # path is what reaches the scalar, never a guess at which field a
689
- # one-field value object might mean. A declaration that names no
690
- # field is refused when the bluebook loads, so nothing has to be
691
- # unwrapped later.
692
- #
693
- # `scalar` below is a different job and stays: rendering a value object
694
- # into a column or a message, where there is no path to consult.
695
-
696
- # A reference is the id itself — refused at the payload gate if it
697
- # arrives as anything else — so there is nothing to open inside it.
698
-
699
- # A reference is an ID, so anything else is not one.
700
- #
701
- # Nothing coerces a reference — `for_attribute` misses on
702
- # "Reference<Account>", which is no value object's name, and hands the
703
- # argument straight through. That is why the wrapped form went in
704
- # unnoticed for as long as it did: there was no place it could be
705
- # refused, so whatever the first caller wrote became the shape.
706
- #
707
- # This is that place. It sits at the payload gate rather than inside
708
- # coercion because the sentence names the command, and `for_attribute`
709
- # never learns which command it is serving.
425
+ # A reference is an ID; refused at the payload gate if it arrives as a
426
+ # Hash or `Runtime::Value` instead. `nil` stays legitimate for an optional
427
+ # reference — a required reference's own `nil` is refused elsewhere (C3.7).
710
428
  #
711
- # Widened past the object shape by BUG#27 (QualityControl ledger,
712
- # found live on `qa/stress_domains/referral_chain`'s `Member.Join`/
713
- # `Referral.Issue`). Without this widening, a bare Boolean, Array, or
714
- # `null` would sail through here untouched — nothing but Hash/Value
715
- # ever refused — then get `.to_s`'d into a lookup key by `CommandRules::
716
- # References#reference_key` ("true", "false", "[8, 8]") and answer
717
- # NotFound, or, for `null`, skip the lookup outright
718
- # (`next if held.nil?`, command_rules/references.rb) and let the
719
- # command run on to whatever its own `given` happened to say — a
720
- # shape error misreading as a missing record, or as an unrelated
721
- # domain refusal. Rust's generated `from_json` requires a JSON
722
- # string for a required reference field before anything else runs
723
- # (`JoinArgs.sponsor: expected String`); this closes the same gate
724
- # at the same DISPATCH_ORDER step Ruby already runs it at
725
- # (`normalize_args`, `Vocabulary::AggregateDispatchOrder`/
726
- # `EntityDispatchOrder`), strictly before `resolve_references` ever
727
- # receives a value to look up — so the two engines now agree on
728
- # both kind and order, not just kind.
729
- #
730
- # `nil` stays legitimate for a `reference_to ..., optional: true`
731
- # argument (`Improvement.Open`'s own `reference_to Angle, optional:
732
- # true` — `qa/bluebook/quality_control.bluebook`): the caller
733
- # genuinely may have nothing to name yet, and `nil_argument`
734
- # (interpreting.rb) already passes an optional reference's `nil`
735
- # through untouched. A required reference offered as `null` is a
736
- # caller leaving a required argument empty in every other sense
737
- # this runtime already refuses (C3.7) — refusing it here, rather
738
- # than falling through to `resolve_references`' own nil-skip and
739
- # then whatever the command's `given` happens to say, is what
740
- # actually names the empty argument instead of something else.
741
- #
742
- # A `has_many` reference's own Array shape is still never refused
743
- # by its wrapper (`Array(value).find { ... }` only inspects the
744
- # list's elements) — a reference is never a scalar list-of-lists
745
- # today, and inventing a rule for a shape the language cannot
746
- # declare is how decoration gets written. `reference_list` (below)
747
- # already owns "not an Array at all" for that case.
748
- #
749
- # @param command [Class] the command class (`Bluebook::Command` subclass)
750
- # `attribute` is declared on, named in a refusal
751
- # @param attribute [Bluebook::Attribute] the attribute to check; a no-op
752
- # unless it is reference-typed
429
+ # @param command [Class] the command `attribute` is declared on, named in a refusal
430
+ # @param attribute [Bluebook::Attribute] the attribute to check; a no-op unless
431
+ # reference-typed
753
432
  # @param value [Object] the offered value
754
433
  # @return [void]
755
- # @raise [Runtime::TypeMismatch] if `value` (or, for a `has_many` reference,
756
- # any of its elements) is a Hash or a `Runtime::Value` rather than a plain
757
- # identity, or a required scalar reference is anything but a String
434
+ # @raise [Runtime::TypeMismatch] if `value` (or an element, for `has_many`) is a
435
+ # Hash or `Runtime::Value` rather than a plain identity
758
436
  def refuse_object_reference(command, attribute, value)
759
437
  return unless attribute.reference?
760
438
 
@@ -775,13 +453,8 @@ module Hecks
775
453
  known_by: known_by(attribute))
776
454
  end
777
455
 
778
- # "an object" for the Hash/Value shape — the original wording this
779
- # method always gave, pinned byte for byte by
780
- # `spec/runtime/reference_shape_spec.rb`, kept unchanged by BUG#27's
781
- # widening. `Rendering.describe` for everything else: `true`,
782
- # `false`, `nil`, `[8, 8]` — the same rendering every other
783
- # TypeMismatch in this file already uses for "here is what you
784
- # actually sent."
456
+ # "an object" for the Hash/Value shape; `Rendering.describe` otherwise —
457
+ # the same rendering every other TypeMismatch in this file uses.
785
458
  #
786
459
  # @param value [Object] the wrongly-shaped offered value to describe
787
460
  # @return [String] `"an object"` for a Hash or `Runtime::Value`; otherwise
@@ -792,22 +465,13 @@ module Hecks
792
465
  Rendering.describe(value)
793
466
  end
794
467
 
795
- # "(Account is known by number)" — what to send instead. No article, on
796
- # purpose: "an Account" and "a Customer" differ by the target's first
797
- # letter, and a refusal pinned byte-for-byte should not hinge on an
798
- # article-choosing rule. Silent when the target is another chapter's,
799
- # where this runtime cannot see what it is known by.
800
- #
801
- # Every head, because a caller has to pass every one. This read
802
- # `identified_by`, which is the single head and is nil the moment an
803
- # identity has two parts — so a composite target fell through the guard
804
- # and the refusal went silent exactly where it had the most to say. A
805
- # single-path target reads as it always did.
468
+ # "(Account is known by number)" — what to send instead. No article, since
469
+ # "an Account" vs "a Customer" would make a pinned refusal hinge on spelling.
806
470
  #
807
471
  # @param attribute [Bluebook::Attribute] the reference-typed attribute to
808
- # describe the target's own identity heads for
809
- # @return [String] `" (Target is known by head1, head2)"`, or `""` when the
810
- # target cannot be resolved or declares no identity heads
472
+ # describe the target's identity heads for
473
+ # @return [String] `" (Target is known by head1, head2)"`, or `""` if the
474
+ # target or its identity heads cannot be resolved
811
475
  def known_by(attribute)
812
476
  heads = Array(attribute.type.resolve&.identity_heads)
813
477
  return "" if heads.empty?
@@ -818,12 +482,9 @@ module Hecks
818
482
  # Renders a value object into the bare scalar its one field holds — for a
819
483
  # column or a message, where there is no path to consult.
820
484
  #
821
- # @param value [Object] the value to render; passed through unless a
822
- # `Runtime::Value`
823
- # @return [Object] `value` unchanged when it is not a `Runtime::Value`;
824
- # otherwise its one field's own value
825
- # @raise [Runtime::TypeMismatch] if `value` is a `Runtime::Value` with more
826
- # than one field
485
+ # @param value [Object] the value to render; passed through unless a `Runtime::Value`
486
+ # @return [Object] `value` unchanged, or its one field's own value
487
+ # @raise [Runtime::TypeMismatch] if `value` has more than one field
827
488
  def scalar(value)
828
489
  return value unless value.is_a?(self)
829
490
 
@@ -835,17 +496,13 @@ module Hecks
835
496
 
836
497
  # Coerces a derived identity string back into `attribute`'s own declared type.
837
498
  #
838
- # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct
499
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] construct
839
500
  # `attribute` is declared on
840
- # @param attribute [Bluebook::Attribute] the identity attribute to coerce
841
- # `identifier` for
842
- # @param identifier [String, Object] the derived identity, typically a
843
- # String (`Identity.of`/`Identity.from`'s own return)
844
- # @return [Runtime::Value, String, Object] a built value object when
845
- # `attribute`'s type names a single-field value object; `identifier`
846
- # unchanged otherwise
847
- # @raise [Runtime::TypeMismatch] if `attribute`'s type names a value object
848
- # with more than one field
501
+ # @param attribute [Bluebook::Attribute] the identity attribute to coerce for
502
+ # @param identifier [String, Object] the derived identity
503
+ # @return [Runtime::Value, String, Object] a built value object for a
504
+ # single-field value-object type; `identifier` unchanged otherwise
505
+ # @raise [Runtime::TypeMismatch] if the type names a multi-field value object
849
506
  def from_identifier(aggregate, attribute, identifier)
850
507
  value_object = value_object_for(aggregate, attribute.type)
851
508
  return identifier unless value_object
@@ -859,31 +516,7 @@ module Hecks
859
516
  raise TypeMismatch, RefusalWording.render_site("TypeMismatch", "composite_identity", type: value_object.hecks_name)
860
517
  end
861
518
 
862
- # Vendored fix, not (yet) upstream hecks (migration plan
863
- # task 9): `identifier` here is always the derived identity
864
- # string -- `Identity.of`/`Identity.from` intentionally return
865
- # one (correct for naming a repository key), and
866
- # `Runtime::Instance#materialize_identity!` calls `from_identifier`
867
- # with exactly that string on every fresh hydration -- but when
868
- # the identity field's own declared type is Integer/Float (not
869
- # the overwhelmingly common String), seeding it straight from
870
- # that string round-trips a correctly-derived identity back in
871
- # as the wrong Ruby type -- and #build's own
872
- # `check_numeric_fields` (added specifically to catch a genuine
873
- # caller mismatch) then refused the runtime's own internal
874
- # identity seed instead, on every dispatch, valid input or not.
875
- #
876
- # Reuses this same file's own `NUMERIC` table (declared-type ->
877
- # expected-Ruby-class, already read by `check_numeric_fields`)
878
- # to decide which declared types need converting, and
879
- # Kernel#Integer/#Float to do the converting. A genuinely
880
- # malformed identifier (should never happen, since an identity
881
- # is always derived from a correctly-typed field in the first
882
- # place, but this stays defensive rather than assume it) passes
883
- # back unconverted, and `check_numeric_fields` refuses it
884
- # exactly as it always has -- preserving its real job of
885
- # catching a genuine caller mismatch, not just this migration's
886
- # own runtime-internal one.
519
+ # Converts a derived numeric identity string back before `check_numeric_fields` runs.
887
520
  private def coerce_identifier(field, identifier)
888
521
  return identifier unless identifier.is_a?(String) && NUMERIC.key?(field.type.to_s)
889
522
 
@@ -905,27 +538,7 @@ module Hecks
905
538
  JSON.generate(fields.sort_by { |name, _| name.to_s }.to_h)
906
539
  end
907
540
 
908
- # A field declared Integer or Float must arrive as one.
909
- #
910
- # Without this a String sails into a numeric field and the failure surfaces
911
- # later, inside a predicate, as `positive? expects a number, got "three"` —
912
- # an EvaluationError, which is not a domain refusal. So the runtime broke
913
- # where the domain should have said no, and the run contract recorded the
914
- # crash beside genuine refusals as though the domain had judged it.
915
- #
916
- # C3.8 — the boundary check for an attribute whose type is a bare
917
- # primitive rather than a value object: `Integer`/`Float` by exact
918
- # numeric class (`NUMERIC`), `String`/booleans by rejecting only a
919
- # composite shape (`COMPOSITE_SHAPES`) — a looser test than
920
- # `check_scalar_shapes` holds a value object's own `String` field to
921
- # (QualityControl BUG#125 has that one also refuse
922
- # a non-string scalar; a bare `String` argument here still admits
923
- # any other scalar — a bare-primitive
924
- # attribute was never part of BUG#125's own investigation or fix,
925
- # and whether it needs the same tightening, and against what real
926
- # Judge dependency if any, is still open); worded by the same
927
- # template with the owning construct as `type`. The Rust boundary is
928
- # stricter there too, recorded in the clause.
541
+ # C3.8 boundary check — refuses a mistyped argument before it breaks a predicate.
929
542
  private def check_bare_primitive(owner, attribute, value)
930
543
  type = attribute.type.to_s
931
544
  expected = NUMERIC[type]
@@ -946,10 +559,8 @@ module Hecks
946
559
  check_numeric_bounds(owner.hecks_name, attribute.name, value)
947
560
  end
948
561
 
949
- # C3.3/C3.4 — the value model's own bounds, held at every boundary:
950
- # an Integer must fit in signed 64 bits, a Float must be finite.
951
- # One check for value-object fields and bare-primitive arguments
952
- # alike (`check_numeric_fields` and `check_bare_primitive`).
562
+ # C3.3/C3.4 — value bounds enforced at every boundary: an Integer must fit
563
+ # signed 64 bits, a Float must be finite.
953
564
  INT64_RANGE = (-(2**63))..((2**63) - 1)
954
565
 
955
566
  private def check_numeric_bounds(type_name, field_name, given)
@@ -965,42 +576,8 @@ module Hecks
965
576
  type: type_name, field: field_name, offered: Rendering.describe(given))
966
577
  end
967
578
 
968
- # QualityControl BUG#41 — a value object refuses a key it does not
969
- # declare, the same way a command's own payload does
970
- # (`CommandInterpreter::ArgumentGate#refuse_unknown_arguments`,
971
- # argument_gate.rb) — reusing that method's exact refusal wording
972
- # (`UnknownArgument unknown_args`, refusal_wording.rb:
973
- # "{command} does not declare {unknown} — it takes {declared}")
974
- # rather than inventing a new template, because every generated
975
- # Rust value-object `from_json` already renders this refusal
976
- # through that identical site: `rust/project/json_codec.rb`'s
977
- # `emit_unknown_argument_check` (mirrored byte-for-byte in
978
- # `rust/codegen/src/json_codec.rs`) emits `v.unknown_keys(&[...])`
979
- # and the same "{name} does not declare {unknown} — it takes
980
- # {declared}" format string for every value object's own
981
- # `from_json` — `GameLabel::from_json`
982
- # (rust/src/generated/chess/game.rs) is simply the first case
983
- # this gap was reproduced against.
984
- #
985
- # Ruby never had an equivalent check anywhere in this `validate!`
986
- # door before now — `fields[attribute.name]` reads only the
987
- # declared attributes, so any other key a caller's Hash carried
988
- # was silently ignored. `fields` here only ever holds what a
989
- # caller (or `for_attribute`'s own recursive coercion) offered
990
- # for this value object — built by `fields_for`'s plain
991
- # key-symbolizing (never a Hash the runtime pads with bookkeeping
992
- # keys of its own; confirmed by reading every call site that
993
- # reaches `validate!`) — so there is nothing legitimate here to
994
- # exempt.
995
- #
996
- # Checked first, before `check_required_fields` and everything
997
- # after it — matching Rust's own `from_json`, which checks
998
- # `unknown_keys` before reading a single declared field. So a
999
- # Hash offering both an unrecognized key and a missing required
1000
- # one (BUG#41's own second demonstration case: `label: {extra:
1001
- # "bogus"}` — unknown and missing `value`) refuses the same
1002
- # UnknownArgument on both engines, not two different refusal
1003
- # kinds for one malformed call.
579
+ # A value object refuses an undeclared key the same way a command's own
580
+ # payload does, checked before any other field-content check.
1004
581
  private def check_unknown_fields(value_object, fields)
1005
582
  known = value_object.attributes.map { |attribute| attribute.name.to_sym }
1006
583
  unknown = (fields.keys.map(&:to_sym) - known).sort
@@ -1013,19 +590,8 @@ module Hecks
1013
590
  declared: declared)
1014
591
  end
1015
592
 
1016
- # C3.7 — a value object is a typed field product: every non-optional
1017
- # field arrives, or construction refuses. A missing field and a null
1018
- # one are the same absence (`fields[name]` reads nil for both), worded
1019
- # as the type mismatch it is — "{type}.{field} expects {expected}, got
1020
- # nil" — the identical string the Rust side's generated `from_json`
1021
- # gives the same input, so the corpus can pin it on both. Checked
1022
- # first among the field-content checks (after `check_unknown_fields`'s
1023
- # own structural gate above, BUG#41): without this, an invariant
1024
- # reading a field that never arrived would be exactly the thing that
1025
- # answers "invariant violated" (or nothing at all — `ToppingName`'s
1026
- # `{value: null}` would be accepted and stored). A `default:` has
1027
- # already been filled in by `apply_defaults`; a list field's absence
1028
- # is an empty list, never a refusal.
593
+ # C3.7 — every non-optional, non-list field must arrive (or construction
594
+ # refuses); a `default:` has already been filled in by `apply_defaults`.
1029
595
  private def check_required_fields(value_object, fields)
1030
596
  value_object.attributes.each do |attribute|
1031
597
  next if attribute.optional? || attribute.list?
@@ -1056,60 +622,15 @@ module Hecks
1056
622
  expected: attribute.type, offered: Rendering.describe(given))
1057
623
  end
1058
624
 
1059
- # PRD 05 (numeric-boundary-coverage) — `given.is_a?(expected)`
1060
- # alone waves a NaN or an Infinity straight through: both are
1061
- # real `Float`s, so `is_a?(Numeric)`/`is_a?(Float)` is true for
1062
- # either. Never exercised before this, because
1063
- # `ValueGenerator::FLOAT_EDGE_CASES` had no non-finite value in
1064
- # it — the fuzzer could not have found this on its own until
1065
- # the table was widened alongside this fix. Left unchecked, a
1066
- # non-finite Float reaches `CommandRules::Arithmetic#clamp`
1067
- # (`current.clamp(min, max)` — `ArgumentError: comparison of
1068
- # Float with X failed`, a genuine Ruby-level crash, not a
1069
- # domain refusal, exactly the same failure mode this method's
1070
- # own header describes for a mistyped field) or all the way to
1071
- # storage, where `JSON.generate`/`#to_json` raises
1072
- # `JSON::GeneratorError: NaN/Infinity not allowed in JSON` the
1073
- # moment anything tries to persist or replay it — again a raw
1074
- # crash, not a refusal. `-0.0` is deliberately not refused
1075
- # here: it is finite, round-trips through JSON as `-0.0`
1076
- # cleanly (confirmed empirically), and is a legitimate,
1077
- # meaningful float value (a signed zero), not a corruption
1078
- # risk — only NaN and +/-Infinity are.
625
+ # `is_a?(expected)` alone waves NaN/Infinity through — both are real
626
+ # Floats. `-0.0` is deliberately left unchecked: finite and legitimate.
1079
627
  check_numeric_bounds(value_object.hecks_name, attribute.name, given)
1080
628
  end
1081
629
  end
1082
630
 
1083
- # A field declared `String` (or a boolean) must not arrive as a
1084
- # composite — an Array or a Hash (or a nested Value) standing in for
1085
- # what has to be a leaf scalar. A `String` field, further, must not
1086
- # arrive as any other non-composite scalar either (Integer, Float,
1087
- # true/false) — QualityControl BUG#125, matching Rust's generated
1088
- # `from_json`, which requires a JSON string node for a String-typed
1089
- # field unconditionally and refuses anything else, including a JSON
1090
- # number or boolean. Without this, Ruby would tolerate exactly that
1091
- # (found live: `Chess::Piece.Capture`'s `PieceId`, String-typed,
1092
- # offered a bignum `id` — Ruby let it pass and failed later on an
1093
- # unrelated field, Rust refused on `id` itself, immediately) — except
1094
- # inside `judge_bootstrapping?` (above), the one caller genuinely
1095
- # relying on that leniency; see that flag's own comment for why.
1096
- #
1097
- # `TrueClass`/`FalseClass` stay laxer than `check_numeric_fields`
1098
- # above: for those two, this still only enforces that the shape
1099
- # isn't a collection, not the exact Ruby class — narrower than the
1100
- # `String` case above because BUG#125 investigated and fixed String
1101
- # specifically; a boolean field's own scalar-shape tolerance is a
1102
- # separate, uninvestigated question left exactly as it was. No
1103
- # scalar field, of any declared type, can ever legitimately be
1104
- # handed an Array or a Hash — that shape is always wrong, and always
1105
- # was: `InvalidValueGenerator#array_for_scalar`'s own corruption is
1106
- # deliberately built to be refused (see that file's header), and
1107
- # until this check existed it sailed straight through for a
1108
- # String/boolean field the way it never could for an Integer/Float
1109
- # one (`check_numeric_fields` above already catches an Array offered
1110
- # for those). Found live via bin/fuzz, seed 17 on the fixtures
1111
- # domain : an Array standing in for a single-field identity's
1112
- # declared `String`, `.to_s`'d into a record id downstream.
631
+ # A scalar field (String, or a boolean) must not arrive as a composite
632
+ # (Array/Hash) standing in for a leaf value. A String field additionally
633
+ # must not arrive as any other scalar, except inside `judge_bootstrapping?`.
1113
634
  COMPOSITE_SHAPES = [Array, ::Hash].freeze
1114
635
  NON_NUMERIC_SCALARS = %w[String TrueClass FalseClass].freeze
1115
636
  private def check_scalar_shapes(value_object, fields)
@@ -1131,16 +652,9 @@ module Hecks
1131
652
  end
1132
653
  end
1133
654
 
1134
- # A field declared with a pattern must match it.
1135
- #
1136
- # Beside check_numeric_fields and for the same reason : a value that does
1137
- # not look like what it claims to be is the domain saying no, and it should
1138
- # say so here rather than let the wrong shape travel on and surface as a
1139
- # broken predicate later.
1140
- #
1141
- # Which regexes may be written at all is PatternSubset's job, enforced when
1142
- # the bluebook is declared — so by the time a value arrives here the pattern
1143
- # is already a vetted, unambiguous one, and this is a plain match.
655
+ # A field declared with a pattern must match it, refused as a TypeMismatch
656
+ # rather than surfacing later as a broken predicate. The pattern itself is
657
+ # already vetted by PatternSubset when the bluebook is declared.
1144
658
  private def check_patterns(value_object, fields)
1145
659
  value_object.attributes.each do |attribute|
1146
660
  pattern = attribute.pattern