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,53 +8,23 @@ module Hecks
8
8
  module Expression
9
9
  class EvaluationError < StandardError; end
10
10
 
11
- # The dotted/arithmetic leaf grammar a predicate's `Resolve` node
12
- # bottoms out in — attribute lookups, literals, arithmetic, and the
13
- # growing set of vendored method-call suffixes (`.match?`,
14
- # `.present?`/`.blank?`, `.set?`/`.unset?`, `.split`, `.first`/
15
- # `.last`, `.all?`/`.any?`/`.none?`/`.find`, ...) this file and its
16
- # `resolver/block_predicates.rb` sibling parse and interpret. Each
17
- # leaf's `parse` is a pure function of its own string, cached at the
18
- # `Evaluator` layer; `interpret` reads state/attrs fresh each call.
11
+ # The dotted/arithmetic leaf grammar a predicate's `Resolve` node bottoms out in.
12
+ # `parse` is a pure function of its string; `interpret` reads state/attrs fresh.
19
13
  module Resolver
20
14
  SIGN_TESTS = Hecks::Vocabulary.fetch("SignTest")
21
15
 
22
- # Which Comparison operator each sign test is sugar for, against the
23
- # literal 0 — Vocabulary::SignTest's own compares_via
24
- # (language/bluebook/vocabulary.bluebook), read off the generated
25
- # table rather than typed a second time.
16
+ # Comparison operator each sign test stands for, against the literal 0.
26
17
  SIGN_TEST_OPERATORS = Hecks::Vocabulary.rows("SignTest")
27
18
  .to_h { |row| [row["name"], row["compares_via"]] }
28
19
  .freeze
29
20
 
30
- # The leaf grammar an expression's dotted/arithmetic side parses into.
31
- # Which node a string produces is a pure function of the string, so
32
- # parse() runs once per distinct leaf (folded into Evaluator's own
33
- # ast_cache — see evaluator.rb) and interpret() reads state/attrs
34
- # fresh on every call. Only Lookup — the true leaf — ever touches
35
- # state/attrs.
21
+ # Leaf nodes of the grammar. Only Lookup touches state/attrs.
36
22
  IntegerLiteral = Struct.new(:value, keyword_init: true)
37
23
  FloatLiteral = Struct.new(:value, keyword_init: true)
38
24
  StringLiteral = Struct.new(:value, keyword_init: true)
39
25
  BoolLiteral = Struct.new(:value, keyword_init: true)
40
- # `["active", "suspended"]` — a literal set, the haystack half of
41
- # an `.include?`. Vocabulary::IncludeHaystack has always admitted
42
- # Array (and Evaluator#includes? has always had a `when Array`
43
- # arm), but nothing could produce one: there was no array-literal
44
- # node, so `["a", "b"].include?(x)` fell through to Lookup and
45
- # refused with `cannot resolve "[\"a\", \"b\"]"`. A declared
46
- # capability with no way to spell it.
47
26
  ArrayLiteral = Struct.new(:elements, keyword_init: true)
48
- # A plain class, not `Struct.new(keyword_init: true)` — every
49
- # sibling leaf node here carries at least one field, but this
50
- # one carries none by nature (a nil literal has no data to
51
- # hold), and `Struct.new` with zero member names ahead of
52
- # `keyword_init:` is real, live Ruby-version-dependent
53
- # behavior: works on 3.3, raises "wrong number of arguments
54
- # (given 0, expected 1+)" on 3.2 (caught deploying to Lambda's
55
- # own ruby3.2 runtime). `.new`/`case ... when NilLiteral` below
56
- # are the only two things this type is ever used for, and a
57
- # bare class answers both identically.
27
+ # A plain class: `Struct.new(keyword_init: true)` with no members raises on Ruby 3.2.
58
28
  NilLiteral = Class.new
59
29
  Addition = Struct.new(:left, :right, keyword_init: true)
60
30
  SignTest = Struct.new(:operator, :test, :receiver, keyword_init: true)
@@ -64,186 +34,39 @@ module Hecks
64
34
  Size = Struct.new(:receiver, keyword_init: true)
65
35
  Lookup = Struct.new(:path, keyword_init: true)
66
36
 
67
- # Update 2026-08-27: every "vendored addition, not (yet) upstream
68
- # hecks" comment on this file's own MatchesRegex/Presence/Split/
69
- # First/Last/StartsWith/EndsWith below (plus ArrayLiteral above)
70
- # described a real gap, found the hard way, in the history each
71
- # comment tells — and that history stays exactly as written,
72
- # on purpose. What changed is the present tense claim "not (yet)
73
- # upstream": a review of this exact migration found these eight
74
- # symbols had working Ruby parse/interpret arms but had never
75
- # gone through Propose -> Render -> Admit
76
- # (lib/hecks/grammar/expression_operators.json) the way every
77
- # other operator here has — a closed-vocabulary guard
78
- # (spec/operator_conformance_spec.rb) built entirely over tables
79
- # structurally could not see hand-coded Struct/parse/interpret
80
- # additions, so eight operators ran in Ruby, admitted nowhere,
81
- # invisible to the one guard whose whole job was "reads in every
82
- # target." All eight are now ledger-admitted for real, with full
83
- # Rust kernel parity (rust/src/kernel/expression_operators/
84
- # {pattern_match,presence,text,positional}.rs) — the two-tier
85
- # gap is closed, not merely tracked.
86
- #
87
- # `receiver.match?(/pattern/)` -- vendored addition, not (yet)
88
- # upstream hecks (migration plan task 8): confirmed the
89
- # single most impactful corpus-wide dispatch-time gap of the
90
- # whole migration -- `.match?(regex)` appears in nearly every
91
- # value_object's format-validation rule across every corpus
92
- # this migration touched (email/phone/ISO-8601-timestamp/zip
93
- # patterns, dozens of files), and had no parse support at all:
94
- # it fell all the way through to the `Lookup` catch-all below,
95
- # which tried to split the entire ".match?(/\A\d{5}\z/)" text
96
- # on "." as if it were a dotted attribute path, and crashed with
97
- # an opaque "no implicit conversion of Symbol into Integer"
98
- # somewhere downstream -- confirmed live via a real dispatch,
99
- # not validate (validate never evaluates a predicate body).
100
- # `receiver` still needs its own parse (it may itself be a
101
- # dotted lookup, e.g. `some_field.match?(...)`), the pattern
102
- # text is taken as-is between the slashes (a Ruby Regexp literal,
103
- # not the evaluator's own mini-grammar -- there is nothing to
104
- # recurse into).
105
37
  MatchesRegex = Struct.new(:receiver, :pattern, :flags, keyword_init: true)
106
38
 
107
- # `.present?`/`.blank?` -- vendored addition, not (yet) upstream
108
- # hecks (migration plan task 8): the second-most pervasive
109
- # gap this pass found, right behind `.match?` -- confirmed real
110
- # via the same "no such attribute" Lookup-fallback crash, since
111
- # neither suffix existed in this leaf grammar at all. Rails-
112
- # standard semantics (blank = nil, or responds_to?(:empty?) &&
113
- # empty? ; present = !blank), not just `!nil?` -- matches what
114
- # every corpus author actually means by `.present?` on a string
115
- # field that could legitimately be "" rather than absent.
116
39
  Presence = Struct.new(:receiver, :negated, keyword_init: true)
117
40
 
118
- # `.set?`/`.unset?` -- sibling of `.present?`/`.blank?` immediately
119
- # above, added for a deliberately narrower question, spelled out in
120
- # the name so choosing between the two pairs is a choice, not a
121
- # trap: `.present?` means "not EMPTY" (Rails-standard -- nil/false,
122
- # and an empty String/Array/Hash, are blank; nothing else is),
123
- # which quietly answers "was this ever assigned" and "does the
124
- # assigned value happen to be empty" as the same question. For an
125
- # optional field whose only legitimate unset state is nil, that
126
- # conflation is a real trap -- confirmed live: a corpus author
127
- # reaching for `superseded_by.blank?` to guard an optional
128
- # reference burned real time before landing on "just don't guard
129
- # it," because `.blank?` cannot be asked to mean ONLY "was this
130
- # never assigned." `.set?`/`.unset?` ask exactly that, and nothing
131
- # else: `!receiver.nil?`, full stop -- an assigned-but-empty
132
- # String/Array/Hash (or a VO wrapping one) is `.set?`, not
133
- # `.unset?`, unlike `.blank?`'s own reading of the identical value.
41
+ # `.set?`/`.unset?` ask only `!nil?`; unlike `.blank?`, an assigned empty value is set.
134
42
  Assignment = Struct.new(:receiver, :negated, keyword_init: true)
135
43
 
136
- # `receiver.split("SEP")` -- vendored addition, not (yet) upstream
137
- # hecks (migration plan task 9): confirmed the single
138
- # highest-value new finding of the real-dispatch smoke sweep --
139
- # the bus's own core `Phrase` value object (dispatch/lexicon/
140
- # query/command_bus.bluebook, all four storehouse-kernel files,
141
- # byte-identical text) validates its four-segment shape with
142
- # `value.split("::").length == 4 && value.split("::").all? { |s|
143
- # s.length > 0 }` -- `.split(` matched none of this grammar's
144
- # known suffixes, so it fell through to the `Lookup` catch-all,
145
- # which split the raw expression text on "." (not the runtime
146
- # value) and crashed with `TypeError: no implicit conversion of
147
- # Symbol into Integer` the moment `String#[]` was handed a
148
- # Symbol segment -- meaning every command taking a Phrase was
149
- # entirely undispatchable, confirmed live via `Lexicon::Lexicon.
150
- # Lookup`/`Query::Query.Run`, not inferred. Produces a real
151
- # Array, deliberately not a scalar -- `.length`/`.all?` compose
152
- # with it below the same way they compose with any other
153
- # receiver, because `parse` recurses on the receiver text and
154
- # the existing `Size` node (`.length`/`.size`) already treats
155
- # "whatever `interpret` returns" as its receiver's value, not a
156
- # fixed type. Separator is taken literally between the quotes,
157
- # same "no sub-grammar to recurse into" precedent `MatchesRegex`
158
- # already set for its own `/pattern/` text above.
159
44
  Split = Struct.new(:receiver, :separator, keyword_init: true)
160
45
 
161
- # `receiver.last` -- vendored addition, not (yet) upstream
162
- # hecks (migration plan task 9), same pass as `Split` above
163
- # and built for the same `Phrase`/`Path` value objects --
164
- # `Query::Phrase`'s own invariant chains `.split("::").last.
165
- # match?(...)` to check the fourth segment's casing. Same shape
166
- # as `.length`/`.size` (Size) and `.empty?` (Empty) -- a plain
167
- # receiver-in, scalar-out accessor, no sub-grammar of its own.
168
46
  Last = Struct.new(:receiver, keyword_init: true)
169
47
 
170
- # `receiver.first` -- sibling addition to `Last` immediately
171
- # above, same shape, same vendored-not-upstream status. Added
172
- # for the shipping domain's `legs.first` (an itinerary's
173
- # departure leg), the exact mirror of `legs.last` (its arrival
174
- # leg) that domain already leaned on -- `Last`'s own duck-typed-
175
- # on-`respond_to?` reasoning applies unchanged, so `first_of`
176
- # below is `last_of` with the one method swapped, not a new
177
- # design.
178
48
  First = Struct.new(:receiver, keyword_init: true)
179
49
 
180
- # `receiver.start_with?("prefix")` / `receiver.end_with?("suffix")`
181
- # -- vendored addition, not (yet) upstream hecks (migration
182
- # plan task 9), the sibling gap the `Split`/`Last`/`BlockPredicate`
183
- # pass above flagged and deliberately left unfixed --
184
- # `Query::Query.Run`/`Dispatch::Dispatch.Route`/`CommandBus::
185
- # CommandBus.Route`'s shared `Params` value object validates its
186
- # JSON-object shape with `value.start_with?("{") && value.
187
- # end_with?("}")` (dispatch/query/command_bus.bluebook, all three
188
- # storehouse-kernel files, byte-identical text) -- `.start_with?(`/
189
- # `.end_with?(` matched none of this grammar's known suffixes, so
190
- # both fell through to the `Lookup` catch-all and crashed with the
191
- # same `TypeError: no implicit conversion of Symbol into
192
- # Integer` shape any unsupported suffix produces here, confirmed
193
- # live via a real dispatch (not validate), not inferred. Two
194
- # separate node types rather than one `mode:`-keyed struct (the `BlockPredicate`/
195
- # `SignTest` precedent) -- `start_with?`/`end_with?` aren't two
196
- # spellings of the same test the way `all?`/`any?`/`none?` are (one
197
- # Array-aggregation family) or `positive?`/`negative?`/`zero?` are
198
- # (one comparison-against-0 family) ; they check different ends of
199
- # the same string and share nothing but their
200
- # receiver-plus-literal-argument shape. `substring` is taken
201
- # literally between the quotes, same "no sub-grammar to recurse
202
- # into" precedent `Split`'s `separator`/`MatchesRegex`'s `pattern`
203
- # already set.
204
50
  StartsWith = Struct.new(:receiver, :substring, keyword_init: true)
205
51
  EndsWith = Struct.new(:receiver, :substring, keyword_init: true)
206
52
 
207
53
  module_function
208
54
 
209
- # Parses and interprets `expr` in one step, without going through
210
- # `Evaluator`'s own boolean/comparison grammar or its `ast_cache` —
211
- # for a caller that wants this leaf grammar's own raw value (an
212
- # Integer, a String, an Array, ...), not a boolean.
55
+ # Parses and interprets `expr` in one step, bypassing `Evaluator`'s boolean grammar.
213
56
  #
214
- # @param expr [String] the dotted/arithmetic leaf expression to
215
- # resolve
216
- # @param state [Hash{Symbol => Object}] the stored attribute values
217
- # an unqualified name may resolve against
218
- # @param attrs [Hash{Symbol => Object}] the call's own argument
219
- # values, checked before `state`
220
- # @return [Object] whatever `expr` resolves to: an Integer, Float,
221
- # String, `true`/`false`, `nil`, or Array, depending on `expr`'s
222
- # own leaf grammar
223
- # @raise [EvaluationError] if `expr` names an attribute or argument
224
- # neither `attrs` nor `state` has, or if an operation in `expr`
225
- # is applied to a value of the wrong type
57
+ # @param expr [String] the dotted/arithmetic leaf expression
58
+ # @param state [Hash{Symbol => Object}] stored attribute values
59
+ # @param attrs [Hash{Symbol => Object}] call arguments, checked before `state`
60
+ # @return [Object] the resolved Integer, Float, String, boolean, nil, or Array
61
+ # @raise [EvaluationError] on an unknown name or a wrongly typed operand
226
62
  def resolve(expr, state, attrs)
227
63
  interpret(parse(expr), state, attrs)
228
64
  end
229
65
 
230
- # A grammar's own dispatch table — one `return Node.new(...) if
231
- # expr =~ /pattern/` per leaf production, in a fixed precedence
232
- # order (`.length` before the generic suffixes, sign tests before
233
- # the rest, etc. — see this file's own history comments on why
234
- # several of these were added in exactly this position). Each
235
- # branch is already one line; splitting the table into smaller
236
- # methods would not shrink any single check, only hide the
237
- # precedence order this method's own line-by-line sequence is.
66
+ # Branch order is the precedence order (`.length` first, sign tests before the rest).
238
67
  #
239
- # @param expr [String] the leaf expression text to parse
240
- # @return [Object] the parsed leaf node — one of this module's own
241
- # Struct types (`IntegerLiteral`, `FloatLiteral`, `StringLiteral`,
242
- # `BoolLiteral`, `NilLiteral`, `ArrayLiteral`, `Addition`,
243
- # `SignTest`, `Empty`, `ToS`, `Modulo`, `Size`, `MatchesRegex`,
244
- # `Presence`, `Assignment`, `Split`, `First`, `Last`,
245
- # `StartsWith`, `EndsWith`, `BlockPredicate`, `Find`), chosen by
246
- # `expr`'s own shape, or `Lookup` when nothing else matches
68
+ # @param expr [String] the leaf expression text
69
+ # @return [Object] the parsed leaf node, or `Lookup` when nothing else matches
247
70
  # rubocop:disable-next Metrics/AbcSize
248
71
  # rubocop:disable-next Metrics/CyclomaticComplexity
249
72
  # rubocop:disable-next Metrics/MethodLength
@@ -313,13 +136,6 @@ module Hecks
313
136
  Lookup.new(path: expr)
314
137
  end
315
138
 
316
- # Builds the `SignTest` node for a matched `.positive?`/
317
- # `.negative?`/`.zero?`-style suffix.
318
- #
319
- # @param parts [Array(String, String)] the `[receiver_text,
320
- # test_name]` pair `match_suffix` returns
321
- # @return [SignTest] the parsed sign-test node, its `receiver`
322
- # itself parsed
323
139
  def sign_test_node(parts)
324
140
  receiver, test = parts
325
141
  symbol = SIGN_TEST_OPERATORS.fetch(test)
@@ -327,28 +143,10 @@ module Hecks
327
143
  SignTest.new(operator: operator, test: test, receiver: parse(receiver))
328
144
  end
329
145
 
330
- # `parse`'s own dispatch table, mirrored: one `when` per leaf node
331
- # type it can produce, each already delegating to a small named
332
- # helper (add, apply_sign_test, emptiness_of, ...) — the case
333
- # itself is the closed, declared set this method exists to
334
- # exhaust; the `else` backstop's own comment explains why a
335
- # missing arm is a bug this method is built to make loud, not
336
- # quiet.
146
+ # Evaluates a node `parse` produced against `state` and `attrs`.
337
147
  #
338
- # @param node [Object] a node `parse` produced (one of this
339
- # module's own Struct types) or built directly by a caller
340
- # @param state [Hash{Symbol => Object}] the stored attribute values
341
- # a `Lookup` node may resolve against
342
- # @param attrs [Hash{Symbol => Object}] the call's own argument
343
- # values, checked before `state`
344
- # @return [Object] the value `node` resolves to: an Integer, Float,
345
- # String, `true`/`false`, `nil`, or Array, depending on `node`'s
346
- # own type
347
- # @raise [EvaluationError] if `node` is not one of the handled
348
- # types, or if a helper it dispatches to (`add`, `apply_sign_test`,
349
- # `matches_regex?`, `split_value`, `last_of`, `first_of`,
350
- # `starts_with?`, `ends_with?`, `apply_modulo`, `size_of`,
351
- # `emptiness_of`, `lookup`) refuses its operand
148
+ # @return [Object] the value the node resolves to
149
+ # @raise [EvaluationError] if the node type is unhandled or an operand is refused
352
150
  # rubocop:disable-next Metrics/AbcSize
353
151
  # rubocop:disable-next Metrics/CyclomaticComplexity
354
152
  # rubocop:disable-next Metrics/MethodLength
@@ -394,42 +192,16 @@ module Hecks
394
192
  when Lookup
395
193
  lookup(node.path, state, attrs)
396
194
  else
397
- # Every leaf node `parse` can produce has a `when` above —
398
- # a backstop against the day this grammar grows a new leaf
399
- # type (this file's own history: MatchesRegex/Presence/
400
- # Split/Last/First/Find/StartsWith/EndsWith/BlockPredicate
401
- # were each added exactly this way, and each one — before it had an
402
- # `interpret` arm — fell all the way through to the
403
- # `Lookup` catch-all in `parse` and crashed downstream with
404
- # an opaque type error, never here). A missing arm here
405
- # would instead return bare `nil` silently, the one wrong-
406
- # answer shape this leaf grammar has otherwise never
407
- # allowed.
195
+ # A missing arm must raise; silently returning nil is the one wrong answer
196
+ # this grammar never allows.
408
197
  raise EvaluationError, "no interpreter handles #{node.class} — add a case before parse can produce it"
409
198
  end
410
199
  end
411
200
 
412
- # `.present?`/`.blank?` -- vendored addition, see the
413
- # `Presence` struct's own comment above. `nil` and `false` are
414
- # blank ; a String/Array/Hash is blank when empty, not merely
415
- # falsy -- a VO-wrapped field that is assigned (`{value: "x"}`,
416
- # `Value#to_h`'d first) is present regardless of what its own
417
- # inner value holds, matching how every VO-typed field in this
418
- # corpus is actually shaped once set at all.
419
- #
420
- # @param value [Object] the already-interpreted receiver value
421
- # @return [Boolean] `true` for `nil`, `false`, or an empty
422
- # String/Array/Hash (a to_h-able value is converted first);
423
- # `false` for anything else
424
201
  def blank?(value)
425
202
  return true if value.nil? || value == false
426
203
 
427
- # Duck-typed, not `value.is_a?(Runtime::Value)` -- this module
428
- # is `Bluebook::Expression`, a different namespace tree from
429
- # `Runtime::Value` entirely, and reaching across for a single
430
- # class check is the exact cross-module coupling that already
431
- # broke `Modulo`'s own `match_call` reference elsewhere in
432
- # this file (found live while building this fix, not assumed).
204
+ # Duck-typed: reaching across to Runtime::Value would couple the namespaces.
433
205
  value = value.to_h if value.respond_to?(:to_h) && !value.is_a?(Hash)
434
206
  case value
435
207
  when String, Array, Hash then value.empty?
@@ -437,26 +209,6 @@ module Hecks
437
209
  end
438
210
  end
439
211
 
440
- # `receiver.match?(/pattern/)` -- vendored addition, see the
441
- # `MatchesRegex` struct's own comment above. `receiver_value` is
442
- # coerced to a plain String first -- inlined here rather than
443
- # calling `Evaluator#string_of` (a different module_function
444
- # module; not actually in scope from inside Resolver despite
445
- # `Modulo`'s own parse rule above calling a same-named
446
- # `match_call` that has the identical cross-module problem --
447
- # found live while building this, not assumed).
448
- #
449
- # @param receiver_value [String, Symbol, Integer, Float, nil] the
450
- # already-interpreted receiver value
451
- # @param pattern [String] the regex source, exactly as it appeared
452
- # between the `/.../` delimiters
453
- # @param flags [String] the suffix flag letters after the closing
454
- # `/`; `"i"` enables `Regexp::IGNORECASE`, `"m"` `MULTILINE`,
455
- # `"x"` `EXTENDED`
456
- # @return [Boolean] whether `pattern` matches `receiver_value`'s
457
- # string form
458
- # @raise [EvaluationError] if `receiver_value` is not a scalar, or
459
- # `pattern` is not a valid regex
460
212
  def matches_regex?(receiver_value, pattern, flags)
461
213
  text = case receiver_value
462
214
  when String, Symbol, Integer, Float then receiver_value.to_s
@@ -472,25 +224,10 @@ module Hecks
472
224
 
473
225
  Regexp.new(pattern, options).match?(text)
474
226
  rescue RegexpError => e
475
- # M9: a malformed pattern between the slashes (an unclosed
476
- # character class, say) is a defect in the expression text
477
- # itself, exactly the same category of author mistake an
478
- # unresolvable attribute name already refuses for — `Regexp.new`
479
- # raising a raw `RegexpError` crossed this sublanguage's own
480
- # refusal boundary the same way the `ZeroDivisionError`/
481
- # `TypeError` cases elsewhere in this file did.
227
+ # A malformed pattern is an author mistake; refuse it as EvaluationError.
482
228
  raise EvaluationError, "match? given an invalid pattern #{pattern.inspect} — #{e.message}"
483
229
  end
484
230
 
485
- # The elements of a bracketed literal, or nil if this isn't one.
486
- # Splits on top-level commas only — quote-aware and depth-aware,
487
- # the same discipline `split_addition` already applies, so a
488
- # nested array or a comma inside a string element stays whole.
489
- #
490
- # @param expr [String] the leaf expression text
491
- # @return [Array<String>, nil] the unparsed source text of each
492
- # element, in order (`[]` for `"[]"`), or `nil` when `expr` is
493
- # not bracketed
494
231
  def array_elements(expr)
495
232
  return nil unless expr.start_with?("[") && expr.end_with?("]")
496
233
 
@@ -523,24 +260,8 @@ module Hecks
523
260
  elements.reject(&:empty?)
524
261
  end
525
262
 
526
- # Braces count toward depth, exactly as parens do — a `+` inside a
527
- # block predicate's own `{ |x| ... }` body is not this expression's
528
- # own addition. `parse` tries addition before `parse_block_opener`,
529
- # so a paren-only depth count split
530
- # kings.any? { |k| k.square.file == to.file + 1 && ... }
531
- # at that inner `+`, turning the whole expression into a nonsense
532
- # Addition whose left operand then walked "any? { |k| k" into an
533
- # Array as an attribute path — "TypeError: no implicit conversion
534
- # of Symbol into Integer", the same signature every unsupported
535
- # construct raises, which is what let this hide. Found live in a
536
- # downstream chess domain's castling given; the evaluator's own
537
- # top_level_index has counted braces since its own version of this
538
- # exact lesson.
539
- #
540
- # @param expr [String] the leaf expression text
541
- # @return [Array(String, String), nil] the `[left, right]` operand
542
- # text around the first top-level `+`, or `nil` when `expr` has
543
- # none
263
+ # Braces and brackets count toward depth like parens, so a `+` inside a block body
264
+ # or an array element is not this expression's own addition.
544
265
  def split_addition(expr)
545
266
  depth = 0
546
267
  quote = nil
@@ -550,17 +271,7 @@ module Hecks
550
271
  quote = nil if char == quote
551
272
  elsif ['"', "'"].include?(char)
552
273
  quote = char
553
- # `[`/`]` -- the identical lesson this method's own `(`/`{`
554
- # comment already names, a third time (found live via the
555
- # type-directed bounded-exhaustive expression generator,
556
- # Phase 7 of the equivalence-gap plan): `ArrayLiteral` can
557
- # appear as a general sub-expression, not only as
558
- # `.include?`'s own haystack, so without counting `[`/`]`
559
- # toward depth here too, an array element containing its own
560
- # top-level `+` (`[0, 0 + 0]`) reads as this expression's own
561
- # addition split point -- tearing the whole receiver before
562
- # `.all?`/`.any?`/etc. in half before `parse_block_opener`
563
- # ever sees it as one atomic leaf.
274
+ # `[`/`]` count too: an array element may hold its own top-level `+`.
564
275
  elsif ["(", "{", "["].include?(char)
565
276
  depth += 1
566
277
  elsif [")", "}", "]"].include?(char)
@@ -572,23 +283,9 @@ module Hecks
572
283
  nil
573
284
  end
574
285
 
575
- # Integer is signed 64-bit (C3.3, docs/semantics/bluebook-semantics.md)
576
- # — Ruby's own Integer would promote past ±2^63-1 silently; the
577
- # language does not, so a sum outside that range is an evaluation
578
- # fault, worded exactly as the Rust kernel's `checked_add` words
579
- # it. A Float sum that is not finite is the same fault (C3.4).
286
+ # Integer is signed 64-bit; a sum outside the range is an evaluation fault (C3.3, C3.4).
580
287
  INT64_RANGE = (-(2**63))..((2**63) - 1)
581
288
 
582
- # Adds two already-interpreted numeric operands, refusing an
583
- # out-of-range result rather than letting it wrap or promote
584
- # silently.
585
- #
586
- # @param left [Integer, Float] the left operand
587
- # @param right [Integer, Float] the right operand
588
- # @return [Integer, Float] `left + right`
589
- # @raise [EvaluationError] if either operand is not numeric, or the
590
- # Integer sum falls outside signed 64-bit range, or the Float sum
591
- # is not finite
592
289
  def add(left, right)
593
290
  lhs = require_number(left, "addition")
594
291
  rhs = require_number(right, "addition")
@@ -603,12 +300,6 @@ module Hecks
603
300
  raise EvaluationError, "addition overflowed: #{lhs} + #{rhs} is not a finite number"
604
301
  end
605
302
 
606
- # Reports whether `expr` is a whole string literal — `"` or `'`
607
- # delimited, both ends.
608
- #
609
- # @param expr [String] the leaf expression text
610
- # @return [Boolean] whether `expr` opens and closes with matching
611
- # quote characters
612
303
  def quoted?(expr)
613
304
  return false if expr.length < 2
614
305
 
@@ -616,135 +307,54 @@ module Hecks
616
307
  (expr.start_with?("'") && expr.end_with?("'"))
617
308
  end
618
309
 
619
- # Declared the same way in Vocabulary::SizedType
620
- # (language/bluebook/vocabulary.bluebook) — spec/vocabulary_conformance_spec
621
- # holds this equal to the language. Shared by .size and .empty?,
622
- # which admit the same set for the same reason.
310
+ # Held equal to Vocabulary::SizedType by spec/vocabulary_conformance_spec.
623
311
  SIZED_TYPES = Hecks::Vocabulary.fetch("SizedType")
624
312
 
625
- # Counts `value`'s elements or characters, for `.length`/`.size`.
626
- #
627
- # @param value [Array, String, Hash] the already-interpreted
628
- # receiver value
629
- # @return [Integer] the number of elements (Array/Hash) or
630
- # characters (String) `value` holds
631
- # @raise [EvaluationError] if `value` is not an Array, String, or
632
- # Hash
633
313
  def size_of(value)
634
314
  return value.size if value.is_a?(Array) || value.is_a?(String) || value.is_a?(Hash)
635
315
 
636
316
  raise EvaluationError, "size expects a list or string, got #{describe(value)}"
637
317
  end
638
318
 
639
- # Reports whether `value` holds no elements or characters, for
640
- # `.empty?`.
641
- #
642
- # @param value [Array, String, Hash] the already-interpreted
643
- # receiver value
644
- # @return [Boolean] whether `value` holds no elements or characters
645
- # @raise [EvaluationError] if `value` is not an Array, String, or
646
- # Hash
647
319
  def emptiness_of(value)
648
320
  return value.empty? if value.is_a?(Array) || value.is_a?(String) || value.is_a?(Hash)
649
321
 
650
322
  raise EvaluationError, "empty? expects a list or string, got #{describe(value)}"
651
323
  end
652
324
 
653
- # Splits `value` on `separator`, for `.split("SEP")` -- vendored
654
- # addition, see the `Split` struct's own comment above. Only a
655
- # String receiver makes sense to split -- unlike `.length`/`.size`/
656
- # `.empty?`, which are already meaningful over Array/Hash too,
657
- # `.split` is a String-only method in the corpus's own usage
658
- # (every occurrence found this pass splits a Phrase's own string
659
- # value).
660
- #
661
- # @param value [String] the already-interpreted receiver value
662
- # @param separator [String] the literal separator text
663
- # @return [Array<String>] `value` split on `separator`, Ruby's own
664
- # `String#split` semantics
665
- # @raise [EvaluationError] if `value` is not a String
666
325
  def split_value(value, separator)
667
326
  raise EvaluationError, "split expects a string, got #{describe(value)}" unless value.is_a?(String)
668
327
 
669
328
  value.split(separator)
670
329
  end
671
330
 
672
- # Returns `value`'s last element, for `.last` -- vendored
673
- # addition, see the `Last` struct's own comment above. Duck-typed
674
- # on `respond_to?(:last)` rather than hard-coding Array -- the one
675
- # corpus usage found this pass (`Query::Phrase`'s
676
- # `.split("::").last`) always receives a `Split`-produced Array,
677
- # but nothing about `.last` itself is Array-specific, and this
678
- # matches `Empty`/`Size`'s own duck-typed-over-a-known-set
679
- # precedent without inventing a narrower rule than the method
680
- # needs.
681
- #
682
- # @param value [#last] the already-interpreted receiver value
683
- # @return [Object] `value.last`
684
- # @raise [EvaluationError] if `value` does not respond to `:last`
685
331
  def last_of(value)
686
332
  return value.last if value.respond_to?(:last)
687
333
 
688
334
  raise EvaluationError, "last expects a list, got #{describe(value)}"
689
335
  end
690
336
 
691
- # Returns `value`'s first element, for `.first` -- see the
692
- # `First` struct's own comment above. `last_of` with the one
693
- # method swapped, same duck-typed reasoning.
694
- #
695
- # @param value [#first] the already-interpreted receiver value
696
- # @return [Object] `value.first`
697
- # @raise [EvaluationError] if `value` does not respond to `:first`
698
337
  def first_of(value)
699
338
  return value.first if value.respond_to?(:first)
700
339
 
701
340
  raise EvaluationError, "first expects a list, got #{describe(value)}"
702
341
  end
703
342
 
704
- # Reports whether `value` starts with `substring`, for
705
- # `.start_with?("prefix")` -- vendored addition, see the
706
- # `StartsWith` struct's own comment above. String-only, same
707
- # reasoning as `.split` above -- every corpus usage found this
708
- # pass (`Params`'s own JSON-object-shape invariant) receives a
709
- # plain String field.
710
- #
711
- # @param value [String] the already-interpreted receiver value
712
- # @param substring [String] the literal prefix text
713
- # @return [Boolean] whether `value` starts with `substring`
714
- # @raise [EvaluationError] if `value` is not a String
715
343
  def starts_with?(value, substring)
716
344
  raise EvaluationError, "start_with? expects a string, got #{describe(value)}" unless value.is_a?(String)
717
345
 
718
346
  value.start_with?(substring)
719
347
  end
720
348
 
721
- # Reports whether `value` ends with `substring`, for
722
- # `.end_with?("suffix")` -- vendored addition, see the `EndsWith`
723
- # struct's own comment above. Same String-only reasoning as
724
- # `start_with?` immediately above.
725
- #
726
- # @param value [String] the already-interpreted receiver value
727
- # @param substring [String] the literal suffix text
728
- # @return [Boolean] whether `value` ends with `substring`
729
- # @raise [EvaluationError] if `value` is not a String
730
349
  def ends_with?(value, substring)
731
350
  raise EvaluationError, "end_with? expects a string, got #{describe(value)}" unless value.is_a?(String)
732
351
 
733
352
  value.end_with?(substring)
734
353
  end
735
354
 
736
- # Declared the same way in Vocabulary::ToStringType
737
- # (language/bluebook/vocabulary.bluebook) — spec/vocabulary_conformance_spec
738
- # holds this equal to the language.
355
+ # Held equal to Vocabulary::ToStringType by spec/vocabulary_conformance_spec.
739
356
  TO_STRING_TYPES = Hecks::Vocabulary.fetch("ToStringType")
740
357
 
741
- # Coerces `value` to its string form, for `.to_s`.
742
- #
743
- # @param value [String, Integer, Float, Boolean, nil] the
744
- # already-interpreted receiver value
745
- # @return [String] `value` unchanged if already a String,
746
- # `value.to_s` for a numeric or boolean, or `""` for `nil`
747
- # @raise [EvaluationError] if `value` is not one of the above
748
358
  def string_of(value)
749
359
  case value
750
360
  when String then value
@@ -755,16 +365,6 @@ module Hecks
755
365
  end
756
366
  end
757
367
 
758
- # Splits `expr` at the first of `suffixes` it ends with — the
759
- # shared lookup behind every `.positive?`/`.negative?`/`.zero?`-
760
- # style sign test.
761
- #
762
- # @param expr [String] the leaf expression text
763
- # @param suffixes [Array<String>] the admitted suffix names
764
- # (without the leading `.`), tried in order
765
- # @return [Array(String, String), nil] the `[receiver_text,
766
- # matched_suffix]` pair for the first matching suffix, or `nil`
767
- # if none matches
768
368
  def match_suffix(expr, suffixes)
769
369
  suffixes.each do |suffix|
770
370
  marker = ".#{suffix}"
@@ -773,14 +373,6 @@ module Hecks
773
373
  nil
774
374
  end
775
375
 
776
- # Applies `node`'s comparison (`positive?`/`negative?`/`zero?`)
777
- # against the literal `0`, reusing `Evaluator.apply`'s own
778
- # comparison primitives rather than re-deriving them by hand.
779
- #
780
- # @param node [SignTest] the sign-test node being evaluated
781
- # @param value [Object] the already-interpreted receiver value
782
- # @return [Boolean] whether `value` passes `node`'s own sign test
783
- # @raise [EvaluationError] if `value` is not numeric
784
376
  def apply_sign_test(node, value)
785
377
  number = numeric(value)
786
378
  raise EvaluationError, "#{node.test} expects a number, got #{describe(value)}" unless number
@@ -788,58 +380,8 @@ module Hecks
788
380
  Evaluator.apply(node.operator, number, 0)
789
381
  end
790
382
 
791
- # Found live via the type-directed bounded-exhaustive expression
792
- # generator (Phase 7, equivalence-gap plan — spec/
793
- # bounded_exhaustive_expression_spec.rb): `.modulo(`'s own
794
- # argument position accepts any numeric sub-expression, including
795
- # another `.modulo(...)` call — `0.modulo(num_b.modulo(-1))` is
796
- # perfectly well-typed — but `expr.rindex(marker)` finds the
797
- # rightmost (innermost) `.modulo(` in the whole string, not the
798
- # outermost one a nested call needs split at. For that expression
799
- # it found the inner `.modulo(` (inside `num_b.modulo(-1)`) and
800
- # split there, producing a receiver of `"0.modulo(num_b"` and a
801
- # divisor of `"-1)"` — both garbage, both re-parsed as bogus
802
- # `Lookup` paths, both then refusing with "cannot resolve" — a
803
- # silent misparse that happened to fail safe into a real
804
- # `EvaluationError` rather than a raw crash, which is exactly why
805
- # this had gone unnoticed: nothing before this generator existed
806
- # ever fed `.modulo` a nested `.modulo` call, random fuzzing
807
- # essentially never manufactures that specific shape by chance,
808
- # and the resulting refusal looks like an ordinary, correct one
809
- # unless you already know every name this generator's own
810
- # synthetic state declares (real corpus authors would see this as
811
- # a mysterious "cannot resolve" on text they never wrote).
812
- #
813
- # Fixed the same way `split_addition`/`Evaluator.top_level_index`
814
- # already handle nested `(`/`{` elsewhere in this exact file:
815
- # find the first (leftmost, outermost) occurrence of the marker,
816
- # then track paren/quote depth from there to find its own
817
- # matching close — not just strip the string's own trailing `)`
818
- # and hope it belongs to this call.
819
- # Not just the first occurrence, either — `.modulo` also chains
820
- # (`x.modulo(a).modulo(b)`, the receiver of the outer call itself
821
- # ending in a `.modulo(...)` call), a second real shape the
822
- # leftmost-occurrence-only version of this fix still mis-parsed:
823
- # the first `.modulo(`'s own matching close paren lands mid-
824
- # string (right after `a)`, before the second `.modulo(b)`), so
825
- # it correctly fails the "reaches the end" check below and must
826
- # be tried again at the next occurrence rather than giving up.
827
- # Trying occurrences strictly left to right and taking the first
828
- # one whose matching close reaches the string's last character
829
- # handles both shapes with the same rule: for nesting
830
- # (`.modulo(x.modulo(y))`), the leftmost (outer) occurrence's own
831
- # paren-depth tracking already walks straight through the inner
832
- # call to the true final `)`; for chaining, the leftmost
833
- # occurrence's close lands short and is rejected, so the next
834
- # occurrence (the true outermost call) is tried instead.
835
- #
836
- # @param expr [String] the leaf expression text
837
- # @param marker [String] the call-opening text to search for, such
838
- # as `".modulo("`
839
- # @return [Array(String, String), nil] the `[receiver_text,
840
- # argument_text]` pair for the outermost occurrence of `marker`
841
- # whose matching close paren reaches `expr`'s last character, or
842
- # `nil` if none does
383
+ # Finds the outermost `marker` call whose matching `)` ends `expr`. `rindex` would split
384
+ # nested `.modulo(` calls at the inner one, and a first match alone mis-parses chains.
843
385
  def match_call(expr, marker)
844
386
  start = 0
845
387
  while (index = expr.index(marker, start))
@@ -851,17 +393,6 @@ module Hecks
851
393
  nil
852
394
  end
853
395
 
854
- # Finds the index of the `)` that closes the `(` already consumed
855
- # by the caller. `matching_brace` (resolver/block_predicates.rb)'s
856
- # own twin, one bracket pair over: `start` is the index just past
857
- # the opening `(` already consumed by the caller (depth starts at
858
- # 1, not 0, for the same reason).
859
- #
860
- # @param expr [String] the source text to scan
861
- # @param start [Integer] the index just past the already-consumed
862
- # opening `(`
863
- # @return [Integer, nil] the index of the matching `)`, or `nil` if
864
- # `expr` has no balanced close from `start` onward
865
396
  def matching_paren(expr, start)
866
397
  depth = 1
867
398
  quote = nil
@@ -883,32 +414,7 @@ module Hecks
883
414
  nil
884
415
  end
885
416
 
886
- # Both operands are coerced to a real Integer/Float before the
887
- # zero-check, and the check reads the coerced divisor — not the
888
- # raw `divisor_value` (which might not even respond to `.zero?`,
889
- # a String for instance) and not a `.to_i`-truncated stand-in for
890
- # it either. The old order checked a truncated `divisor.to_i`
891
- # after already validating the untruncated value wasn't zero, so
892
- # a divisor merely small (`0.3`, truncating to `0`) sailed past
893
- # the guard and then blew up `Integer#%` with a raw
894
- # `ZeroDivisionError` the moment it reached zero anyway.
895
- #
896
- # The modulo itself is plain `%` on the coerced values, matching
897
- # `add`'s own no-truncation precedent just above — Ruby's native
898
- # `%` already handles every Integer/Float combination correctly
899
- # (promoting to Float when either side is one), so rounding both
900
- # operands down to Integer first was pure data loss with no
901
- # purpose: `7.5.modulo(2.5)` silently became `7 % 2` (`1`)
902
- # instead of the real `0.0`.
903
- #
904
- # @param receiver_value [Object] the already-interpreted receiver
905
- # value
906
- # @param divisor_value [Object] the already-interpreted divisor
907
- # value
908
- # @return [Integer, Float] `receiver % divisor`, on the coerced
909
- # numeric operands
910
- # @raise [EvaluationError] if either operand is not numeric, or the
911
- # coerced divisor is zero
417
+ # The zero-check reads the coerced divisor, not the raw value or a `to_i` truncation.
912
418
  def apply_modulo(receiver_value, divisor_value)
913
419
  receiver = require_number(receiver_value, "modulo")
914
420
  divisor = require_number(divisor_value, "modulo")
@@ -917,20 +423,6 @@ module Hecks
917
423
  receiver % divisor
918
424
  end
919
425
 
920
- # Resolves a `Lookup` node's dotted path against `attrs` (checked
921
- # first) and `state`, unwrapping a single-field value object's
922
- # result to its own scalar.
923
- #
924
- # @param expr [String] the dotted (or bare) attribute path, such as
925
- # `"customer.status"`
926
- # @param state [Hash{Symbol => Object}] the stored attribute values
927
- # the path's root name may resolve against
928
- # @param attrs [Hash{Symbol => Object}] the call's own argument
929
- # values, checked before `state`
930
- # @return [Object] the resolved value, unwrapped by `unwrap_scalar`
931
- # @raise [EvaluationError] if the path's root name is in neither
932
- # `attrs` nor `state`, or a later segment cannot be read from the
933
- # value it navigates onto
934
426
  def lookup(expr, state, attrs)
935
427
  return unwrap_scalar(fetch(expr, state, attrs)) unless expr.include?(".")
936
428
 
@@ -938,25 +430,8 @@ module Hecks
938
430
  unwrap_scalar(walk_path(fetch(head, state, attrs), rest))
939
431
  end
940
432
 
941
- # Walks a list of dotted segments through an already-resolved
942
- # Hash-like value — extracted from `lookup`'s own reduce so
943
- # `found_of` (the `Find` node's own path projection) can walk a
944
- # `.find { ... }`-produced element the identical way `lookup`
945
- # walks a plain attribute path, rather than duplicating the
946
- # symbol-or-string key step twice in this file. `key?` decides
947
- # which spelling answers — a bare `||` between the two would
948
- # treat a genuinely-held `false` the same as an absent key and
949
- # fall through to the other spelling, landing on `nil`.
950
- #
951
- # @param value [Object] the already-resolved starting value; `nil`
952
- # once any segment lands on a value that does not respond to
953
- # `#[]`
954
- # @param segments [Array<String>] the dotted path's remaining
955
- # segments, in order
956
- # @return [Object, nil] the value reached by walking every segment,
957
- # or `nil` if a segment lands on a value with no `#[]`
958
- # @raise [EvaluationError] if a segment cannot be read from an
959
- # Array-like value (a non-Integer/Range index)
433
+ # Walks dotted segments through a Hash-like value. `key?` picks the symbol or string
434
+ # spelling so a held `false` is not mistaken for an absent key.
960
435
  def walk_path(value, segments)
961
436
  segments.reduce(value) do |current, segment|
962
437
  break nil unless current.respond_to?(:[])
@@ -968,15 +443,7 @@ module Hecks
968
443
  begin
969
444
  current[segment]
970
445
  rescue TypeError
971
- # M9 (docs/audits/2026-08-10-main-bug-audit.md): a dotted
972
- # path can walk onto an Array (e.g. the result of `.split`,
973
- # or a `list_of` attribute) — Array#[] demands an
974
- # Integer/Range and raises a raw TypeError for a String
975
- # segment ("no implicit conversion of String into
976
- # Integer"). This rescue catches it and re-raises as this
977
- # sublanguage's own refusal ("this predicate doesn't apply
978
- # here"), rather than letting the raw TypeError cross this
979
- # sublanguage's own refusal boundary and crash the runtime.
446
+ # `Array#[]` raises a raw TypeError for a String segment; refuse it instead.
980
447
  raise EvaluationError,
981
448
  "cannot read #{segment.inspect} from #{describe(current)}"
982
449
  end
@@ -984,64 +451,9 @@ module Hecks
984
451
  end
985
452
  end
986
453
 
987
- # Unwraps a single-field value object to its own scalar, so
988
- # `field == "literal"` -- vendored addition, not (yet) upstream
989
- # hecks (migration plan task 8): the third-most pervasive
990
- # dispatch-time gap this pass found, same family as `.match?`/
991
- # `.present?` above -- a lookup of a single-field
992
- # scalar-convenience value object (the exact shape `Value.
993
- # from_identifier`/`Value::Coercion#fields_for`'s own single-
994
- # field auto-unwrap already treats as "this VO IS its scalar"
995
- # everywhere else in this runtime) came back as the `Value`
996
- # wrapper itself, never unwrapped for reading -- so `Value#==`
997
- # (which only ever equals another `Value` instance) silently
998
- # refused every `guarantees "..." do status == "active" end` /
999
- # `expects "..." do trash_day.present? end`-shaped bare
1000
- # comparison against a raw literal. Confirmed corpus-wide, not
1001
- # one file's mistake: bin-buddy alone has this exact `field ==
1002
- # "literal"` shape in plan.bluebook, service_task.bluebook,
1003
- # route.bluebook, and subscription.bluebook, all equally silent
1004
- # until a real dispatch (never validate) exercised the
1005
- # predicate.
1006
- #
1007
- # Unwraps both the bare (undotted) case and a dotted lookup's own
1008
- # terminal value: a dotted lookup that navigates through an
1009
- # entity/list element to a nested field (`leg.voyage`, where
1010
- # `voyage` is itself a single-field VO) lands on the same
1011
- # unwrapped-`Value` shape a bare lookup does, and hits the
1012
- # identical silent `Value#==` failure without the same
1013
- # "this VO IS its scalar" treatment — only the intermediate hops
1014
- # need raw `#[]` addressing to keep navigating. `unwrap_scalar` is
1015
- # idempotent on an already-raw scalar (a String/Integer doesn't
1016
- # respond to `#to_h`), so this is safe for `field.value`-shaped
1017
- # dotted lookups too — they already return a raw scalar and stay
1018
- # unaffected.
1019
- #
1020
- # Gated on the sole attribute's *count*, never its *name*: gating
1021
- # on a literal `:value` key would be a lie of omission for
1022
- # `Money{amount}` and every other single-field value object whose
1023
- # author picked a domain name for the field — the same
1024
- # "this VO IS its scalar" reading
1025
- # ([[feedback_name_the_scalar_field]],
1026
- # `Behaviour::ValueObject#sole_attribute`) applies regardless of
1027
- # what the sole field happens to be called, and a name gate would
1028
- # make a bare `balance > 0` work for a `Balance{value}` while
1029
- # silently comparing a whole VO for a `Balance{amount}`. A
1030
- # declared `Runtime::Value` reads its own `sole_attribute` (the
1031
- # declaration's answer, not the stored hash's); any other
1032
- # to_h-able (a Struct, a bespoke wrapper with no declaration to
1033
- # consult) keeps the `{value: X}`-only unwrap, so nothing that
1034
- # never was a value object gains a surprise unwrapping.
1035
- # `rust/src/kernel/json.rs`'s `impl Fielded for Json` mirrors the
1036
- # count-only reading on the Rust side — change them in lockstep
1037
- # or rust_conformance diverges.
1038
- #
1039
- # @param value [Object] a resolved lookup or path-walk result
1040
- # @return [Object] `value` itself if it is a Hash, Array, or has no
1041
- # `#to_h`; a declared `Runtime::Value`'s sole attribute's own
1042
- # value, read via `sole.name`, if it has exactly one attribute;
1043
- # `value[:value]` for any other to_h-able with exactly one key
1044
- # named `:value`; `value` unchanged otherwise
454
+ # Unwraps a single-field value object to its scalar so `field == "literal"` works.
455
+ # Gated on the field count, not its name. Mirrors `impl Fielded for Json` in
456
+ # rust/src/kernel/json.rs; change both together.
1045
457
  def unwrap_scalar(value)
1046
458
  return value unless value.respond_to?(:to_h) && !value.is_a?(Hash) && !value.is_a?(Array)
1047
459
 
@@ -1054,17 +466,6 @@ module Hecks
1054
466
  hash.size == 1 && hash.key?(:value) ? hash[:value] : value
1055
467
  end
1056
468
 
1057
- # Resolves a single unqualified name — `attrs` first, `state`
1058
- # second.
1059
- #
1060
- # @param name [String, Symbol] the attribute or argument name
1061
- # @param state [Hash{Symbol => Object}, #[]] the stored attribute
1062
- # values
1063
- # @param attrs [Hash{Symbol => Object}] the call's own argument
1064
- # values, checked first
1065
- # @return [Object] the value held under `name`
1066
- # @raise [EvaluationError] if `name` is in neither `attrs` nor
1067
- # `state`
1068
469
  def fetch(name, state, attrs)
1069
470
  key = name.to_sym
1070
471
  return attrs[key] if attrs.key?(key)
@@ -1073,45 +474,21 @@ module Hecks
1073
474
  raise EvaluationError, "cannot resolve #{name.inspect} — no such attribute or argument"
1074
475
  end
1075
476
 
1076
- # Reports whether `state` holds `key`, duck-typed for a `state`
1077
- # that does not respond to `:key?`.
1078
- #
1079
- # @param state [Hash{Symbol => Object}, #[]] the stored attribute
1080
- # values
1081
- # @param key [Symbol] the attribute name to look for
1082
- # @return [Boolean] `state.key?(key)` when `state` responds to
1083
- # `:key?`; otherwise whether `state[key]` is non-nil
1084
477
  def known?(state, key)
1085
478
  return state.key?(key) if state.respond_to?(:key?)
1086
479
 
1087
480
  !state[key].nil?
1088
481
  end
1089
482
 
1090
- # Returns `value` if it is already numeric, or `nil` if it is not.
1091
- #
1092
- # @param value [Object] the value to check
1093
- # @return [Integer, Float, nil] `value` unchanged if it is an
1094
- # Integer or Float, otherwise `nil`
1095
483
  def numeric(value)
1096
484
  value if value.is_a?(Integer) || value.is_a?(Float)
1097
485
  end
1098
486
 
1099
- # Coerces `value` to a number or refuses it.
1100
- #
1101
- # @param value [Object] the value to check
1102
- # @param operation [String] the operation name, for the refusal
1103
- # message
1104
- # @return [Integer, Float] `value` unchanged
1105
- # @raise [EvaluationError] if `value` is not an Integer or Float
1106
487
  def require_number(value, operation)
1107
488
  numeric(value) ||
1108
489
  raise(EvaluationError, "#{operation} expects a number, got #{describe(value)}")
1109
490
  end
1110
491
 
1111
- # Renders `value` the way it should read inside a refusal message.
1112
- #
1113
- # @param value [Object] the value to render
1114
- # @return [String] `Rendering.describe(value)`
1115
492
  def describe(value) = Rendering.describe(value)
1116
493
  end
1117
494
  end