hecks 1.3.0 → 1.5.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 (373) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +72 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +83 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
  7. data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
  9. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  10. data/lib/hecks/adapters/driven/heki.rb +106 -9
  11. data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
  12. data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
  13. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  15. data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
  16. data/lib/hecks/adapters/driven/lambda.rb +82 -35
  17. data/lib/hecks/adapters/driven/local_storage.rb +83 -10
  18. data/lib/hecks/adapters/driven/memory.rb +205 -9
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -1
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  24. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  25. data/lib/hecks/adapters/driven/prism.rb +50 -11
  26. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  31. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  32. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  33. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  34. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  35. data/lib/hecks/adapters/driven.rb +6 -4
  36. data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
  37. data/lib/hecks/behaviors/dsl.rb +60 -2
  38. data/lib/hecks/behaviors/expectations.rb +190 -29
  39. data/lib/hecks/behaviors/ir.rb +12 -1
  40. data/lib/hecks/behaviors/rspec.rb +9 -1
  41. data/lib/hecks/behaviors/runner.rb +21 -2
  42. data/lib/hecks/behaviors.rb +9 -1
  43. data/lib/hecks/bluebook/aggregate.rb +43 -13
  44. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
  45. data/lib/hecks/bluebook/assembly/build.rb +26 -5
  46. data/lib/hecks/bluebook/assembly/contract.rb +98 -23
  47. data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
  48. data/lib/hecks/bluebook/assembly/marks.rb +159 -30
  49. data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
  50. data/lib/hecks/bluebook/assembly.rb +32 -14
  51. data/lib/hecks/bluebook/attribute.rb +26 -12
  52. data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
  53. data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
  54. data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
  55. data/lib/hecks/bluebook/behaviour/command.rb +55 -25
  56. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
  57. data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
  58. data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
  59. data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
  60. data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
  61. data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
  62. data/lib/hecks/bluebook/behaviour/query.rb +6 -1
  63. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
  64. data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
  65. data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
  66. data/lib/hecks/bluebook/capabilities.rb +27 -0
  67. data/lib/hecks/bluebook/chapter.rb +51 -9
  68. data/lib/hecks/bluebook/command.rb +62 -17
  69. data/lib/hecks/bluebook/domain_port.rb +34 -9
  70. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  71. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  72. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +282 -123
  73. data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
  74. data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
  75. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
  76. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
  77. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  78. data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
  79. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  80. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  81. data/lib/hecks/bluebook/dsl/entity_builder.rb +191 -61
  82. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  83. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
  84. data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
  85. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  86. data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
  87. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  88. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  89. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +111 -47
  90. data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
  91. data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
  92. data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
  93. data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
  94. data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
  95. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  96. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  97. data/lib/hecks/bluebook/entity.rb +40 -11
  98. data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
  99. data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
  100. data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
  101. data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
  102. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
  103. data/lib/hecks/bluebook/expression/resolver.rb +369 -128
  104. data/lib/hecks/bluebook/hexagon.rb +35 -1
  105. data/lib/hecks/bluebook/lifecycle.rb +12 -1
  106. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
  107. data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
  108. data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
  109. data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
  110. data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
  111. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
  112. data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
  113. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
  114. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
  115. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
  116. data/lib/hecks/bluebook/meta_validator.rb +235 -139
  117. data/lib/hecks/bluebook/model_check.rb +434 -104
  118. data/lib/hecks/bluebook/pattern_subset.rb +32 -10
  119. data/lib/hecks/bluebook/policy.rb +15 -13
  120. data/lib/hecks/bluebook/process_manager.rb +27 -14
  121. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  122. data/lib/hecks/bluebook/project_loader.rb +40 -0
  123. data/lib/hecks/bluebook/project_register.rb +50 -6
  124. data/lib/hecks/bluebook/query.rb +31 -4
  125. data/lib/hecks/bluebook/read_model.rb +35 -15
  126. data/lib/hecks/bluebook/reference.rb +26 -13
  127. data/lib/hecks/bluebook/smoke_test.rb +46 -23
  128. data/lib/hecks/bluebook/synthesizer.rb +46 -12
  129. data/lib/hecks/bluebook/translation.rb +34 -5
  130. data/lib/hecks/bluebook/value_object.rb +29 -11
  131. data/lib/hecks/bluebook.rb +5 -6
  132. data/lib/hecks/codemod.rb +138 -50
  133. data/lib/hecks/construct.rb +21 -7
  134. data/lib/hecks/corpus.rb +438 -0
  135. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  136. data/lib/hecks/doc/reference.rb +200 -31
  137. data/lib/hecks/embryonaut_bluebook.rb +38 -15
  138. data/lib/hecks/facade/cli_door.rb +69 -10
  139. data/lib/hecks/facade/cli_runner.rb +105 -24
  140. data/lib/hecks/facade/command_request.rb +23 -0
  141. data/lib/hecks/facade/handle.rb +155 -35
  142. data/lib/hecks/facade/json_door.rb +106 -25
  143. data/lib/hecks/facade/surface/aggregate_door.rb +50 -27
  144. data/lib/hecks/facade/surface/chapter.rb +26 -17
  145. data/lib/hecks/facade/surface.rb +16 -3
  146. data/lib/hecks/facade.rb +15 -4
  147. data/lib/hecks/forms/app.rb +46 -30
  148. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  149. data/lib/hecks/forms/field_renderer.rb +142 -6
  150. data/lib/hecks/forms/field_shape.rb +183 -19
  151. data/lib/hecks/forms/html.rb +51 -7
  152. data/lib/hecks/forms/index_renderer.rb +14 -2
  153. data/lib/hecks/forms/page.rb +14 -0
  154. data/lib/hecks/forms/params.rb +120 -23
  155. data/lib/hecks/forms/port_argument.rb +14 -2
  156. data/lib/hecks/forms/query_form_renderer.rb +65 -2
  157. data/lib/hecks/forms/record_renderer.rb +60 -2
  158. data/lib/hecks/forms/record_table.rb +28 -1
  159. data/lib/hecks/forms/reference_options.rb +24 -0
  160. data/lib/hecks/forms/value_object_shape.rb +13 -3
  161. data/lib/hecks/forms.rb +24 -4
  162. data/lib/hecks/fqn.rb +59 -1
  163. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  164. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  165. data/lib/hecks/framework/oidc.json +15 -0
  166. data/lib/hecks/framework.rb +79 -25
  167. data/lib/hecks/freezer.rb +27 -11
  168. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
  169. data/lib/hecks/fuzzing/combination_miner.rb +178 -0
  170. data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
  171. data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
  172. data/lib/hecks/fuzzing/differential.rb +192 -0
  173. data/lib/hecks/fuzzing/domain_generator.rb +871 -0
  174. data/lib/hecks/fuzzing/era_boundary.rb +68 -19
  175. data/lib/hecks/fuzzing/form_census.rb +121 -20
  176. data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
  177. data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
  178. data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
  179. data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
  180. data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
  181. data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
  182. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
  183. data/lib/hecks/fuzzing/properties/guards.rb +86 -42
  184. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
  185. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
  186. data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
  187. data/lib/hecks/fuzzing/properties/querying.rb +82 -28
  188. data/lib/hecks/fuzzing/properties.rb +84 -51
  189. data/lib/hecks/fuzzing/qa_settings.rb +164 -0
  190. data/lib/hecks/fuzzing/replay.rb +241 -133
  191. data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
  192. data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
  193. data/lib/hecks/fuzzing/self_consistency.rb +315 -137
  194. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
  195. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
  196. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
  197. data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
  198. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
  199. data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
  200. data/lib/hecks/fuzzing/shrinker.rb +309 -0
  201. data/lib/hecks/fuzzing/structural_skips.rb +37 -130
  202. data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
  203. data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
  204. data/lib/hecks/fuzzing/value_generator.rb +110 -22
  205. data/lib/hecks/fuzzing.rb +5 -0
  206. data/lib/hecks/grammar/evolve.rb +188 -12
  207. data/lib/hecks/grammar.rb +53 -7
  208. data/lib/hecks/ir.rb +51 -20
  209. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  210. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  211. data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
  212. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  213. data/lib/hecks/language/oidc.json +5 -0
  214. data/lib/hecks/literal.rb +41 -9
  215. data/lib/hecks/naming.rb +112 -31
  216. data/lib/hecks/ports/access_control.rb +53 -2
  217. data/lib/hecks/ports/agent/answers.rb +83 -6
  218. data/lib/hecks/ports/agent.rb +119 -35
  219. data/lib/hecks/ports/authentication.rb +44 -4
  220. data/lib/hecks/ports/authorization.rb +50 -11
  221. data/lib/hecks/ports/clock.rb +42 -23
  222. data/lib/hecks/ports/extraction.rb +16 -0
  223. data/lib/hecks/ports/identity_assignment.rb +23 -2
  224. data/lib/hecks/ports/identity_generation.rb +17 -3
  225. data/lib/hecks/ports/identity_resolution.rb +17 -1
  226. data/lib/hecks/ports/key_vault.port +6 -0
  227. data/lib/hecks/ports/key_vault.rb +58 -0
  228. data/lib/hecks/ports/loading.rb +4 -0
  229. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  230. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  231. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  232. data/lib/hecks/ports/persistence/execution.rb +4 -0
  233. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  234. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  235. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  236. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  237. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  238. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  239. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +34 -5
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  250. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
  251. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  252. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +95 -6
  253. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  254. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
  255. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  256. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  257. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  258. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +48 -8
  259. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  260. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  261. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  262. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +58 -19
  263. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
  264. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  265. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -1
  266. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  267. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  268. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  269. data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
  270. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  271. data/lib/hecks/ports/persistence.rb +36 -1
  272. data/lib/hecks/ports/projection.rb +61 -7
  273. data/lib/hecks/ports/query/in_memory.rb +41 -8
  274. data/lib/hecks/ports/query/ordering.rb +21 -6
  275. data/lib/hecks/ports/query.rb +35 -0
  276. data/lib/hecks/ports.rb +1 -0
  277. data/lib/hecks/projections/bootstrap_table.rb +147 -0
  278. data/lib/hecks/projections/diagrams.rb +315 -79
  279. data/lib/hecks/projections/glossary/html.rb +91 -3
  280. data/lib/hecks/projections/glossary/markdown.rb +50 -3
  281. data/lib/hecks/projections/glossary/mermaid.rb +49 -1
  282. data/lib/hecks/projections/glossary/sections.rb +1 -1
  283. data/lib/hecks/projections/glossary/sentences.rb +110 -5
  284. data/lib/hecks/projections/glossary.rb +173 -31
  285. data/lib/hecks/projections/ir.rb +1 -1
  286. data/lib/hecks/projections/model/deviations.rb +62 -17
  287. data/lib/hecks/projections/model.rb +75 -21
  288. data/lib/hecks/projections/oidc.rb +45 -8
  289. data/lib/hecks/projections/parser_table.rb +53 -12
  290. data/lib/hecks/projections/reference.rb +15 -3
  291. data/lib/hecks/projections/rust_vocabulary.rb +646 -0
  292. data/lib/hecks/projections/shape.rb +10 -2
  293. data/lib/hecks/projections/statements.rb +69 -22
  294. data/lib/hecks/projections/vocabulary.rb +26 -9
  295. data/lib/hecks/projections.rb +5 -3
  296. data/lib/hecks/projector/cli_projector.rb +245 -37
  297. data/lib/hecks/projector/docs_projector.rb +154 -28
  298. data/lib/hecks/projector/exporter.rb +104 -29
  299. data/lib/hecks/projector/ir_projector.rb +7 -1
  300. data/lib/hecks/projector/narrate_projector.rb +144 -23
  301. data/lib/hecks/projector/target.rb +42 -18
  302. data/lib/hecks/projector.rb +86 -18
  303. data/lib/hecks/query_ir.rb +94 -47
  304. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  305. data/lib/hecks/query_specification/common/comparison.rb +109 -27
  306. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  307. data/lib/hecks/query_specification/common/null_policy.rb +61 -15
  308. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  309. data/lib/hecks/query_specification/common/options.rb +25 -0
  310. data/lib/hecks/query_specification/field_path.rb +69 -15
  311. data/lib/hecks/query_specification/hop_path.rb +57 -20
  312. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  313. data/lib/hecks/rendering.rb +9 -3
  314. data/lib/hecks/router/namespace_installer.rb +16 -3
  315. data/lib/hecks/router.rb +56 -1
  316. data/lib/hecks/runtime/aggregate_lock.rb +20 -11
  317. data/lib/hecks/runtime/boot_gates.rb +21 -3
  318. data/lib/hecks/runtime/caller.rb +40 -8
  319. data/lib/hecks/runtime/capability_graph.rb +13 -2
  320. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
  321. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
  322. data/lib/hecks/runtime/command_interpreter.rb +195 -113
  323. data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
  324. data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
  325. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  326. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  327. data/lib/hecks/runtime/command_rules/references.rb +145 -55
  328. data/lib/hecks/runtime/command_rules.rb +3 -0
  329. data/lib/hecks/runtime/dependency_planning.rb +56 -11
  330. data/lib/hecks/runtime/dispatcher.rb +263 -92
  331. data/lib/hecks/runtime/entity_element.rb +304 -67
  332. data/lib/hecks/runtime/entity_interpreter.rb +149 -88
  333. data/lib/hecks/runtime/errors.rb +37 -23
  334. data/lib/hecks/runtime/event.rb +15 -10
  335. data/lib/hecks/runtime/identity.rb +91 -23
  336. data/lib/hecks/runtime/instance.rb +103 -18
  337. data/lib/hecks/runtime/interpreting.rb +24 -16
  338. data/lib/hecks/runtime/invocation.rb +358 -0
  339. data/lib/hecks/runtime/loader.rb +105 -19
  340. data/lib/hecks/runtime/outbox.rb +164 -26
  341. data/lib/hecks/runtime/policy_interpreter.rb +73 -60
  342. data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
  343. data/lib/hecks/runtime/query_interpreter.rb +93 -74
  344. data/lib/hecks/runtime/reaction_invocation.rb +73 -28
  345. data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
  346. data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
  347. data/lib/hecks/runtime/reference_hop.rb +48 -6
  348. data/lib/hecks/runtime/refusal_wording.rb +142 -115
  349. data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
  350. data/lib/hecks/runtime/registry/verification.rb +153 -28
  351. data/lib/hecks/runtime/registry.rb +202 -32
  352. data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
  353. data/lib/hecks/runtime/routing.rb +36 -154
  354. data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
  355. data/lib/hecks/runtime/saga_interpreter.rb +90 -76
  356. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  357. data/lib/hecks/runtime/tenant_check.rb +33 -13
  358. data/lib/hecks/runtime/tenant_scope.rb +23 -5
  359. data/lib/hecks/runtime/value/admission.rb +75 -30
  360. data/lib/hecks/runtime/value/coercion.rb +555 -142
  361. data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
  362. data/lib/hecks/runtime/value.rb +71 -21
  363. data/lib/hecks/runtime.rb +39 -7
  364. data/lib/hecks/storehouse.rb +368 -72
  365. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  366. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  367. data/lib/hecks/version.rb +3 -3
  368. data/lib/hecks/vocabulary.rb +205 -4
  369. data/lib/hecks.rb +91 -11
  370. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
  371. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
  372. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
  373. metadata +28 -2
@@ -20,14 +20,12 @@ module Hecks
20
20
  SIGN_TESTS = Hecks::Vocabulary.fetch("SignTest")
21
21
 
22
22
  # Which Comparison operator each sign test is sugar for, against the
23
- # literal 0 — declared the same way in Vocabulary::SignTest's
24
- # compares_via (language/bluebook/vocabulary.bluebook) ; spec/vocabulary_conformance_spec
25
- # holds the two tables equal.
26
- SIGN_TEST_OPERATORS = {
27
- "positive?" => ">",
28
- "negative?" => "<",
29
- "zero?" => "=="
30
- }.freeze
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.
26
+ SIGN_TEST_OPERATORS = Hecks::Vocabulary.rows("SignTest")
27
+ .to_h { |row| [row["name"], row["compares_via"]] }
28
+ .freeze
31
29
 
32
30
  # The leaf grammar an expression's dotted/arithmetic side parses into.
33
31
  # Which node a string produces is a pure function of the string, so
@@ -40,7 +38,7 @@ module Hecks
40
38
  StringLiteral = Struct.new(:value, keyword_init: true)
41
39
  BoolLiteral = Struct.new(:value, keyword_init: true)
42
40
  # `["active", "suspended"]` — a literal set, the haystack half of
43
- # an `.include?`. Vocabulary::IncludeHaystack has always ADMITTED
41
+ # an `.include?`. Vocabulary::IncludeHaystack has always admitted
44
42
  # Array (and Evaluator#includes? has always had a `when Array`
45
43
  # arm), but nothing could produce one: there was no array-literal
46
44
  # node, so `["a", "b"].include?(x)` fell through to Lookup and
@@ -66,18 +64,18 @@ module Hecks
66
64
  Size = Struct.new(:receiver, keyword_init: true)
67
65
  Lookup = Struct.new(:path, keyword_init: true)
68
66
 
69
- # UPDATE 2026-08-27: every "vendored addition, not (yet) upstream
67
+ # Update 2026-08-27: every "vendored addition, not (yet) upstream
70
68
  # hecks" comment on this file's own MatchesRegex/Presence/Split/
71
69
  # First/Last/StartsWith/EndsWith below (plus ArrayLiteral above)
72
- # described a REAL gap, found the hard way, in the history each
70
+ # described a real gap, found the hard way, in the history each
73
71
  # comment tells — and that history stays exactly as written,
74
- # on purpose. What changed is the PRESENT TENSE claim "not (yet)
72
+ # on purpose. What changed is the present tense claim "not (yet)
75
73
  # upstream": a review of this exact migration found these eight
76
- # symbols had working Ruby parse/interpret arms but had NEVER
74
+ # symbols had working Ruby parse/interpret arms but had never
77
75
  # gone through Propose -> Render -> Admit
78
76
  # (lib/hecks/grammar/expression_operators.json) the way every
79
77
  # other operator here has — a closed-vocabulary guard
80
- # (spec/operator_conformance_spec.rb) built entirely over TABLES
78
+ # (spec/operator_conformance_spec.rb) built entirely over tables
81
79
  # structurally could not see hand-coded Struct/parse/interpret
82
80
  # additions, so eight operators ran in Ruby, admitted nowhere,
83
81
  # invisible to the one guard whose whole job was "reads in every
@@ -88,13 +86,13 @@ module Hecks
88
86
  #
89
87
  # `receiver.match?(/pattern/)` -- vendored addition, not (yet)
90
88
  # upstream hecks (migration plan task 8): confirmed the
91
- # SINGLE most impactful corpus-wide dispatch-time gap of the
89
+ # single most impactful corpus-wide dispatch-time gap of the
92
90
  # whole migration -- `.match?(regex)` appears in nearly every
93
91
  # value_object's format-validation rule across every corpus
94
92
  # this migration touched (email/phone/ISO-8601-timestamp/zip
95
- # patterns, dozens of files), and had NO parse support at all:
93
+ # patterns, dozens of files), and had no parse support at all:
96
94
  # it fell all the way through to the `Lookup` catch-all below,
97
- # which tried to split the ENTIRE ".match?(/\A\d{5}\z/)" text
95
+ # which tried to split the entire ".match?(/\A\d{5}\z/)" text
98
96
  # on "." as if it were a dotted attribute path, and crashed with
99
97
  # an opaque "no implicit conversion of Symbol into Integer"
100
98
  # somewhere downstream -- confirmed live via a real dispatch,
@@ -118,13 +116,13 @@ module Hecks
118
116
  Presence = Struct.new(:receiver, :negated, keyword_init: true)
119
117
 
120
118
  # `.set?`/`.unset?` -- sibling of `.present?`/`.blank?` immediately
121
- # above, added for a DELIBERATELY narrower question, spelled out in
119
+ # above, added for a deliberately narrower question, spelled out in
122
120
  # the name so choosing between the two pairs is a choice, not a
123
121
  # trap: `.present?` means "not EMPTY" (Rails-standard -- nil/false,
124
- # and an EMPTY String/Array/Hash, are blank; nothing else is),
122
+ # and an empty String/Array/Hash, are blank; nothing else is),
125
123
  # which quietly answers "was this ever assigned" and "does the
126
- # assigned value happen to be empty" as the SAME question. For an
127
- # optional field whose only legitimate unset state IS nil, that
124
+ # assigned value happen to be empty" as the same question. For an
125
+ # optional field whose only legitimate unset state is nil, that
128
126
  # conflation is a real trap -- confirmed live: a corpus author
129
127
  # reaching for `superseded_by.blank?` to guard an optional
130
128
  # reference burned real time before landing on "just don't guard
@@ -144,7 +142,7 @@ module Hecks
144
142
  # `value.split("::").length == 4 && value.split("::").all? { |s|
145
143
  # s.length > 0 }` -- `.split(` matched none of this grammar's
146
144
  # known suffixes, so it fell through to the `Lookup` catch-all,
147
- # which split the RAW EXPRESSION TEXT on "." (not the runtime
145
+ # which split the raw expression text on "." (not the runtime
148
146
  # value) and crashed with `TypeError: no implicit conversion of
149
147
  # Symbol into Integer` the moment `String#[]` was handed a
150
148
  # Symbol segment -- meaning every command taking a Phrase was
@@ -190,10 +188,10 @@ module Hecks
190
188
  # storehouse-kernel files, byte-identical text) -- `.start_with?(`/
191
189
  # `.end_with?(` matched none of this grammar's known suffixes, so
192
190
  # both fell through to the `Lookup` catch-all and crashed with the
193
- # identical `TypeError: no implicit conversion of Symbol into
194
- # Integer` shape `.split`/`.all?` used to, confirmed live via a
195
- # real dispatch (not validate), not inferred. Two separate node
196
- # types rather than one `mode:`-keyed struct (the `BlockPredicate`/
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`/
197
195
  # `SignTest` precedent) -- `start_with?`/`end_with?` aren't two
198
196
  # spellings of the same test the way `all?`/`any?`/`none?` are (one
199
197
  # Array-aggregation family) or `positive?`/`negative?`/`zero?` are
@@ -208,18 +206,44 @@ module Hecks
208
206
 
209
207
  module_function
210
208
 
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.
213
+ #
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
211
226
  def resolve(expr, state, attrs)
212
227
  interpret(parse(expr), state, attrs)
213
228
  end
214
229
 
215
230
  # A grammar's own dispatch table — one `return Node.new(...) if
216
- # expr =~ /pattern/` per leaf production, in a FIXED precedence
231
+ # expr =~ /pattern/` per leaf production, in a fixed precedence
217
232
  # order (`.length` before the generic suffixes, sign tests before
218
233
  # the rest, etc. — see this file's own history comments on why
219
234
  # several of these were added in exactly this position). Each
220
235
  # branch is already one line; splitting the table into smaller
221
236
  # methods would not shrink any single check, only hide the
222
- # precedence order this method's own line-by-line sequence IS.
237
+ # precedence order this method's own line-by-line sequence is.
238
+ #
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
223
247
  # rubocop:disable-next Metrics/AbcSize
224
248
  # rubocop:disable-next Metrics/CyclomaticComplexity
225
249
  # rubocop:disable-next Metrics/MethodLength
@@ -289,6 +313,13 @@ module Hecks
289
313
  Lookup.new(path: expr)
290
314
  end
291
315
 
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
292
323
  def sign_test_node(parts)
293
324
  receiver, test = parts
294
325
  symbol = SIGN_TEST_OPERATORS.fetch(test)
@@ -299,10 +330,25 @@ module Hecks
299
330
  # `parse`'s own dispatch table, mirrored: one `when` per leaf node
300
331
  # type it can produce, each already delegating to a small named
301
332
  # helper (add, apply_sign_test, emptiness_of, ...) — the case
302
- # itself IS the closed, declared set this method exists to
333
+ # itself is the closed, declared set this method exists to
303
334
  # exhaust; the `else` backstop's own comment explains why a
304
335
  # missing arm is a bug this method is built to make loud, not
305
336
  # quiet.
337
+ #
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
306
352
  # rubocop:disable-next Metrics/AbcSize
307
353
  # rubocop:disable-next Metrics/CyclomaticComplexity
308
354
  # rubocop:disable-next Metrics/MethodLength
@@ -365,11 +411,16 @@ module Hecks
365
411
 
366
412
  # `.present?`/`.blank?` -- vendored addition, see the
367
413
  # `Presence` struct's own comment above. `nil` and `false` are
368
- # blank ; a String/Array/Hash is blank when EMPTY, not merely
369
- # falsy -- a VO-wrapped field that IS assigned (`{value: "x"}`,
414
+ # blank ; a String/Array/Hash is blank when empty, not merely
415
+ # falsy -- a VO-wrapped field that is assigned (`{value: "x"}`,
370
416
  # `Value#to_h`'d first) is present regardless of what its own
371
417
  # inner value holds, matching how every VO-typed field in this
372
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
373
424
  def blank?(value)
374
425
  return true if value.nil? || value == false
375
426
 
@@ -389,11 +440,23 @@ module Hecks
389
440
  # `receiver.match?(/pattern/)` -- vendored addition, see the
390
441
  # `MatchesRegex` struct's own comment above. `receiver_value` is
391
442
  # coerced to a plain String first -- inlined here rather than
392
- # calling `Evaluator#string_of` (a DIFFERENT module_function
443
+ # calling `Evaluator#string_of` (a different module_function
393
444
  # module; not actually in scope from inside Resolver despite
394
445
  # `Modulo`'s own parse rule above calling a same-named
395
446
  # `match_call` that has the identical cross-module problem --
396
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
397
460
  def matches_regex?(receiver_value, pattern, flags)
398
461
  text = case receiver_value
399
462
  when String, Symbol, Integer, Float then receiver_value.to_s
@@ -410,7 +473,7 @@ module Hecks
410
473
  Regexp.new(pattern, options).match?(text)
411
474
  rescue RegexpError => e
412
475
  # M9: a malformed pattern between the slashes (an unclosed
413
- # character class, say) is a defect in the EXPRESSION TEXT
476
+ # character class, say) is a defect in the expression text
414
477
  # itself, exactly the same category of author mistake an
415
478
  # unresolvable attribute name already refuses for — `Regexp.new`
416
479
  # raising a raw `RegexpError` crossed this sublanguage's own
@@ -420,9 +483,14 @@ module Hecks
420
483
  end
421
484
 
422
485
  # The elements of a bracketed literal, or nil if this isn't one.
423
- # Splits on TOP-LEVEL commas only — quote-aware and depth-aware,
486
+ # Splits on top-level commas only — quote-aware and depth-aware,
424
487
  # the same discipline `split_addition` already applies, so a
425
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
426
494
  def array_elements(expr)
427
495
  return nil unless expr.start_with?("[") && expr.end_with?("]")
428
496
 
@@ -455,9 +523,9 @@ module Hecks
455
523
  elements.reject(&:empty?)
456
524
  end
457
525
 
458
- # BRACES COUNT TOWARD DEPTH, exactly as parens do — a `+` inside a
526
+ # Braces count toward depth, exactly as parens do — a `+` inside a
459
527
  # block predicate's own `{ |x| ... }` body is not this expression's
460
- # own addition. `parse` tries addition BEFORE `parse_block_opener`,
528
+ # own addition. `parse` tries addition before `parse_block_opener`,
461
529
  # so a paren-only depth count split
462
530
  # kings.any? { |k| k.square.file == to.file + 1 && ... }
463
531
  # at that inner `+`, turning the whole expression into a nonsense
@@ -468,6 +536,11 @@ module Hecks
468
536
  # downstream chess domain's castling given; the evaluator's own
469
537
  # top_level_index has counted braces since its own version of this
470
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
471
544
  def split_addition(expr)
472
545
  depth = 0
473
546
  quote = nil
@@ -481,12 +554,13 @@ module Hecks
481
554
  # comment already names, a third time (found live via the
482
555
  # type-directed bounded-exhaustive expression generator,
483
556
  # Phase 7 of the equivalence-gap plan): `ArrayLiteral` can
484
- # appear as a general sub-expression now, not only as
485
- # `.include?`'s own haystack, so an array element containing
486
- # its own top-level `+` (`[0, 0 + 0]`) used to read as THIS
487
- # expression's own addition split point -- the whole
488
- # receiver before `.all?`/`.any?`/etc. torn in half before
489
- # `parse_block_opener` ever saw it as one atomic leaf.
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.
490
564
  elsif ["(", "{", "["].include?(char)
491
565
  depth += 1
492
566
  elsif [")", "}", "]"].include?(char)
@@ -505,6 +579,16 @@ module Hecks
505
579
  # it. A Float sum that is not finite is the same fault (C3.4).
506
580
  INT64_RANGE = (-(2**63))..((2**63) - 1)
507
581
 
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
508
592
  def add(left, right)
509
593
  lhs = require_number(left, "addition")
510
594
  rhs = require_number(right, "addition")
@@ -519,6 +603,12 @@ module Hecks
519
603
  raise EvaluationError, "addition overflowed: #{lhs} + #{rhs} is not a finite number"
520
604
  end
521
605
 
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
522
612
  def quoted?(expr)
523
613
  return false if expr.length < 2
524
614
 
@@ -532,67 +622,111 @@ module Hecks
532
622
  # which admit the same set for the same reason.
533
623
  SIZED_TYPES = Hecks::Vocabulary.fetch("SizedType")
534
624
 
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
535
633
  def size_of(value)
536
634
  return value.size if value.is_a?(Array) || value.is_a?(String) || value.is_a?(Hash)
537
635
 
538
636
  raise EvaluationError, "size expects a list or string, got #{describe(value)}"
539
637
  end
540
638
 
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
541
647
  def emptiness_of(value)
542
648
  return value.empty? if value.is_a?(Array) || value.is_a?(String) || value.is_a?(Hash)
543
649
 
544
650
  raise EvaluationError, "empty? expects a list or string, got #{describe(value)}"
545
651
  end
546
652
 
547
- # `.split("SEP")` -- vendored addition, see the `Split` struct's
548
- # own comment above. Only a String receiver makes sense to
549
- # split -- unlike `.length`/`.size`/`.empty?`, which are already
550
- # meaningful over Array/Hash too, `.split` is a String-only
551
- # method in the corpus's own usage (every occurrence found this
552
- # pass splits a Phrase's own string value).
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
553
666
  def split_value(value, separator)
554
667
  raise EvaluationError, "split expects a string, got #{describe(value)}" unless value.is_a?(String)
555
668
 
556
669
  value.split(separator)
557
670
  end
558
671
 
559
- # `.last` -- vendored addition, see the `Last` struct's own
560
- # comment above. Duck-typed on `respond_to?(:last)` rather than
561
- # hard-coding Array -- the one corpus usage found this pass
562
- # (`Query::Phrase`'s `.split("::").last`) always receives a
563
- # `Split`-produced Array, but nothing about `.last` itself is
564
- # Array-specific, and this matches `Empty`/`Size`'s own
565
- # duck-typed-over-a-known-set precedent without inventing a
566
- # narrower rule than the method needs.
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`
567
685
  def last_of(value)
568
686
  return value.last if value.respond_to?(:last)
569
687
 
570
688
  raise EvaluationError, "last expects a list, got #{describe(value)}"
571
689
  end
572
690
 
573
- # `.first` -- see the `First` struct's own comment above.
574
- # `last_of` with the one method swapped, same duck-typed
575
- # reasoning.
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`
576
698
  def first_of(value)
577
699
  return value.first if value.respond_to?(:first)
578
700
 
579
701
  raise EvaluationError, "first expects a list, got #{describe(value)}"
580
702
  end
581
703
 
704
+ # Reports whether `value` starts with `substring`, for
582
705
  # `.start_with?("prefix")` -- vendored addition, see the
583
706
  # `StartsWith` struct's own comment above. String-only, same
584
707
  # reasoning as `.split` above -- every corpus usage found this
585
708
  # pass (`Params`'s own JSON-object-shape invariant) receives a
586
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
587
715
  def starts_with?(value, substring)
588
716
  raise EvaluationError, "start_with? expects a string, got #{describe(value)}" unless value.is_a?(String)
589
717
 
590
718
  value.start_with?(substring)
591
719
  end
592
720
 
721
+ # Reports whether `value` ends with `substring`, for
593
722
  # `.end_with?("suffix")` -- vendored addition, see the `EndsWith`
594
723
  # struct's own comment above. Same String-only reasoning as
595
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
596
730
  def ends_with?(value, substring)
597
731
  raise EvaluationError, "end_with? expects a string, got #{describe(value)}" unless value.is_a?(String)
598
732
 
@@ -604,6 +738,13 @@ module Hecks
604
738
  # holds this equal to the language.
605
739
  TO_STRING_TYPES = Hecks::Vocabulary.fetch("ToStringType")
606
740
 
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
607
748
  def string_of(value)
608
749
  case value
609
750
  when String then value
@@ -614,6 +755,16 @@ module Hecks
614
755
  end
615
756
  end
616
757
 
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
617
768
  def match_suffix(expr, suffixes)
618
769
  suffixes.each do |suffix|
619
770
  marker = ".#{suffix}"
@@ -622,6 +773,14 @@ module Hecks
622
773
  nil
623
774
  end
624
775
 
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
625
784
  def apply_sign_test(node, value)
626
785
  number = numeric(value)
627
786
  raise EvaluationError, "#{node.test} expects a number, got #{describe(value)}" unless number
@@ -629,50 +788,58 @@ module Hecks
629
788
  Evaluator.apply(node.operator, number, 0)
630
789
  end
631
790
 
632
- # FOUND LIVE via the type-directed bounded-exhaustive expression
791
+ # Found live via the type-directed bounded-exhaustive expression
633
792
  # generator (Phase 7, equivalence-gap plan — spec/
634
793
  # bounded_exhaustive_expression_spec.rb): `.modulo(`'s own
635
794
  # argument position accepts any numeric sub-expression, including
636
- # ANOTHER `.modulo(...)` call — `0.modulo(num_b.modulo(-1))` is
795
+ # another `.modulo(...)` call — `0.modulo(num_b.modulo(-1))` is
637
796
  # perfectly well-typed — but `expr.rindex(marker)` finds the
638
- # RIGHTMOST (innermost) `.modulo(` in the whole string, not the
639
- # OUTERMOST one a nested call needs split at. For that expression
640
- # it found the INNER `.modulo(` (inside `num_b.modulo(-1)`) and
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
641
800
  # split there, producing a receiver of `"0.modulo(num_b"` and a
642
801
  # divisor of `"-1)"` — both garbage, both re-parsed as bogus
643
802
  # `Lookup` paths, both then refusing with "cannot resolve" — a
644
- # SILENT MISPARSE that happened to fail safe into a real
803
+ # silent misparse that happened to fail safe into a real
645
804
  # `EvaluationError` rather than a raw crash, which is exactly why
646
805
  # this had gone unnoticed: nothing before this generator existed
647
806
  # ever fed `.modulo` a nested `.modulo` call, random fuzzing
648
807
  # essentially never manufactures that specific shape by chance,
649
- # and the resulting refusal LOOKS like an ordinary, correct one
808
+ # and the resulting refusal looks like an ordinary, correct one
650
809
  # unless you already know every name this generator's own
651
810
  # synthetic state declares (real corpus authors would see this as
652
811
  # a mysterious "cannot resolve" on text they never wrote).
653
812
  #
654
813
  # Fixed the same way `split_addition`/`Evaluator.top_level_index`
655
814
  # already handle nested `(`/`{` elsewhere in this exact file:
656
- # find the FIRST (leftmost, outermost) occurrence of the marker,
657
- # then track paren/quote depth from there to find ITS OWN
815
+ # find the first (leftmost, outermost) occurrence of the marker,
816
+ # then track paren/quote depth from there to find its own
658
817
  # matching close — not just strip the string's own trailing `)`
659
818
  # and hope it belongs to this call.
660
- # Not just the FIRST occurrence, either — `.modulo` also CHAINS
661
- # (`x.modulo(a).modulo(b)`, the receiver of the OUTER call itself
819
+ # Not just the first occurrence, either — `.modulo` also chains
820
+ # (`x.modulo(a).modulo(b)`, the receiver of the outer call itself
662
821
  # ending in a `.modulo(...)` call), a second real shape the
663
822
  # leftmost-occurrence-only version of this fix still mis-parsed:
664
823
  # the first `.modulo(`'s own matching close paren lands mid-
665
824
  # string (right after `a)`, before the second `.modulo(b)`), so
666
825
  # it correctly fails the "reaches the end" check below and must
667
- # be tried again at the NEXT occurrence rather than giving up.
668
- # Trying occurrences strictly left to right and taking the FIRST
826
+ # be tried again at the next occurrence rather than giving up.
827
+ # Trying occurrences strictly left to right and taking the first
669
828
  # one whose matching close reaches the string's last character
670
- # handles both shapes with the same rule: for NESTING
829
+ # handles both shapes with the same rule: for nesting
671
830
  # (`.modulo(x.modulo(y))`), the leftmost (outer) occurrence's own
672
831
  # paren-depth tracking already walks straight through the inner
673
- # call to the true final `)`; for CHAINING, the leftmost
832
+ # call to the true final `)`; for chaining, the leftmost
674
833
  # occurrence's close lands short and is rejected, so the next
675
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
676
843
  def match_call(expr, marker)
677
844
  start = 0
678
845
  while (index = expr.index(marker, start))
@@ -684,10 +851,17 @@ module Hecks
684
851
  nil
685
852
  end
686
853
 
687
- # `matching_brace` (resolver/block_predicates.rb)'s own twin, one
688
- # bracket pair over: `start` is the index just past the OPENING
689
- # `(` already consumed by the caller (depth starts at 1, not 0,
690
- # for the same reason).
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
691
865
  def matching_paren(expr, start)
692
866
  depth = 1
693
867
  quote = nil
@@ -709,12 +883,12 @@ module Hecks
709
883
  nil
710
884
  end
711
885
 
712
- # Both operands are coerced to a real Integer/Float BEFORE the
713
- # zero-check, and the check reads the COERCED divisor — not the
886
+ # Both operands are coerced to a real Integer/Float before the
887
+ # zero-check, and the check reads the coerced divisor — not the
714
888
  # raw `divisor_value` (which might not even respond to `.zero?`,
715
889
  # a String for instance) and not a `.to_i`-truncated stand-in for
716
890
  # it either. The old order checked a truncated `divisor.to_i`
717
- # AFTER already validating the untruncated value wasn't zero, so
891
+ # after already validating the untruncated value wasn't zero, so
718
892
  # a divisor merely small (`0.3`, truncating to `0`) sailed past
719
893
  # the guard and then blew up `Integer#%` with a raw
720
894
  # `ZeroDivisionError` the moment it reached zero anyway.
@@ -726,6 +900,15 @@ module Hecks
726
900
  # operands down to Integer first was pure data loss with no
727
901
  # purpose: `7.5.modulo(2.5)` silently became `7 % 2` (`1`)
728
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
729
912
  def apply_modulo(receiver_value, divisor_value)
730
913
  receiver = require_number(receiver_value, "modulo")
731
914
  divisor = require_number(divisor_value, "modulo")
@@ -734,6 +917,20 @@ module Hecks
734
917
  receiver % divisor
735
918
  end
736
919
 
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
737
934
  def lookup(expr, state, attrs)
738
935
  return unwrap_scalar(fetch(expr, state, attrs)) unless expr.include?(".")
739
936
 
@@ -750,6 +947,16 @@ module Hecks
750
947
  # which spelling answers — a bare `||` between the two would
751
948
  # treat a genuinely-held `false` the same as an absent key and
752
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)
753
960
  def walk_path(value, segments)
754
961
  segments.reduce(value) do |current, segment|
755
962
  break nil unless current.respond_to?(:[])
@@ -766,10 +973,10 @@ module Hecks
766
973
  # or a `list_of` attribute) — Array#[] demands an
767
974
  # Integer/Range and raises a raw TypeError for a String
768
975
  # segment ("no implicit conversion of String into
769
- # Integer"), which used to cross straight past this
770
- # sublanguage's own refusal boundary and crash the
771
- # runtime instead of reading as "this predicate doesn't
772
- # apply here."
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.
773
980
  raise EvaluationError,
774
981
  "cannot read #{segment.inspect} from #{describe(current)}"
775
982
  end
@@ -777,6 +984,7 @@ module Hecks
777
984
  end
778
985
  end
779
986
 
987
+ # Unwraps a single-field value object to its own scalar, so
780
988
  # `field == "literal"` -- vendored addition, not (yet) upstream
781
989
  # hecks (migration plan task 8): the third-most pervasive
782
990
  # dispatch-time gap this pass found, same family as `.match?`/
@@ -785,7 +993,7 @@ module Hecks
785
993
  # from_identifier`/`Value::Coercion#fields_for`'s own single-
786
994
  # field auto-unwrap already treats as "this VO IS its scalar"
787
995
  # everywhere else in this runtime) came back as the `Value`
788
- # wrapper itself, never unwrapped for READING -- so `Value#==`
996
+ # wrapper itself, never unwrapped for reading -- so `Value#==`
789
997
  # (which only ever equals another `Value` instance) silently
790
998
  # refused every `guarantees "..." do status == "active" end` /
791
999
  # `expects "..." do trash_day.present? end`-shaped bare
@@ -794,48 +1002,46 @@ module Hecks
794
1002
  # "literal"` shape in plan.bluebook, service_task.bluebook,
795
1003
  # route.bluebook, and subscription.bluebook, all equally silent
796
1004
  # until a real dispatch (never validate) exercised the
797
- # predicate. Scoped narrowly to the single-field `{value: X}`
798
- # shape only.
799
- #
800
- # UPDATE 2026-08-18: originally scoped to unwrap ONLY the bare
801
- # (undotted) case, on the belief that a dotted lookup only ever
802
- # reaches into a VO's OWN field (`field.value`, `field.sub_
803
- # field`) and so should keep walking `#[]` untouched. That
804
- # belief held for the single-hop case but not for the general
805
- # one: a dotted lookup that NAVIGATES THROUGH an entity/list
806
- # element to a nested field (`leg.voyage`, where `voyage` is
807
- # itself a single-field VO) landed on the very same unwrapped-
808
- # `Value` shape the bare case fixed, and hit the identical
809
- # silent `Value#==` failure -- comparing it against a raw
810
- # literal or another unwrapped VO returned false for everything,
811
- # no error. The terminal value of a dotted walk deserves the
812
- # same "this VO IS its scalar" treatment as a bare lookup's
813
- # result; only the INTERMEDIATE hops need raw `#[]` addressing
814
- # to keep navigating. `unwrap_scalar` is idempotent on an
815
- # already-raw scalar (a String/Integer doesn't respond to
816
- # `#to_h`), so this is safe for the existing `field.value`-
817
- # shaped dotted lookups too -- they already returned a raw
818
- # scalar and are unaffected.
819
- # UPDATE (single-element value objects strictly answer `.value`):
820
- # the unwrap used to gate on the sole key being literally NAMED
821
- # `:value` — correct for the shorthand/closed-set shapes that
822
- # motivated it, but a lie of omission for `Money{amount}` and
823
- # every other single-field value object whose author picked a
824
- # domain name for the field: the SAME "this VO IS its scalar"
825
- # reading ([[feedback_name_the_scalar_field]], `Behaviour::
826
- # ValueObject#sole_attribute`) applies regardless of what the
827
- # sole field happens to be called, and the name gate made a bare
828
- # `balance > 0` work for a `Balance{value}` while silently
829
- # comparing a whole VO for a `Balance{amount}`. Now the COUNT is
830
- # the gate, never the name. A declared `Runtime::Value` reads
831
- # its own `sole_attribute` (the declaration's answer, not the
832
- # stored hash's); any OTHER to_h-able (a Struct, a bespoke
833
- # wrapper with no declaration to consult) keeps the original
834
- # `{value: X}`-only unwrap, so nothing that never was a value
835
- # object gains a surprise unwrapping. `rust/src/kernel/json.rs`'s
836
- # `impl Fielded for Json` mirrors the count-only reading on the
837
- # Rust side — change them in lockstep or rust_conformance
838
- # diverges.
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
839
1045
  def unwrap_scalar(value)
840
1046
  return value unless value.respond_to?(:to_h) && !value.is_a?(Hash) && !value.is_a?(Array)
841
1047
 
@@ -848,6 +1054,17 @@ module Hecks
848
1054
  hash.size == 1 && hash.key?(:value) ? hash[:value] : value
849
1055
  end
850
1056
 
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`
851
1068
  def fetch(name, state, attrs)
852
1069
  key = name.to_sym
853
1070
  return attrs[key] if attrs.key?(key)
@@ -856,21 +1073,45 @@ module Hecks
856
1073
  raise EvaluationError, "cannot resolve #{name.inspect} — no such attribute or argument"
857
1074
  end
858
1075
 
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
859
1084
  def known?(state, key)
860
1085
  return state.key?(key) if state.respond_to?(:key?)
861
1086
 
862
1087
  !state[key].nil?
863
1088
  end
864
1089
 
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`
865
1095
  def numeric(value)
866
1096
  value if value.is_a?(Integer) || value.is_a?(Float)
867
1097
  end
868
1098
 
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
869
1106
  def require_number(value, operation)
870
1107
  numeric(value) ||
871
1108
  raise(EvaluationError, "#{operation} expects a number, got #{describe(value)}")
872
1109
  end
873
1110
 
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)`
874
1115
  def describe(value) = Rendering.describe(value)
875
1116
  end
876
1117
  end