hecks 1.4.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 (267) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +65 -0
  3. data/lib/hecks/adapters/driven/folder.rb +73 -0
  4. data/lib/hecks/adapters/driven/google_authentication.rb +25 -4
  5. data/lib/hecks/adapters/driven/governance_authorization.rb +57 -6
  6. data/lib/hecks/adapters/driven/heki/journal.rb +8 -0
  7. data/lib/hecks/adapters/driven/heki/saga_store.rb +53 -7
  8. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  9. data/lib/hecks/adapters/driven/heki.rb +93 -2
  10. data/lib/hecks/adapters/driven/identity_registry.rb +10 -0
  11. data/lib/hecks/adapters/driven/in_memory_ordering.rb +22 -0
  12. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  13. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  14. data/lib/hecks/adapters/driven/lambda/client.rb +35 -7
  15. data/lib/hecks/adapters/driven/lambda.rb +43 -2
  16. data/lib/hecks/adapters/driven/local_storage.rb +67 -1
  17. data/lib/hecks/adapters/driven/memory.rb +13 -13
  18. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +20 -0
  19. data/lib/hecks/adapters/driven/prism.rb +46 -7
  20. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  21. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  22. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  23. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  24. data/lib/hecks/adapters/driven.rb +2 -0
  25. data/lib/hecks/adapters/driving/github_webhook.rb +14 -1
  26. data/lib/hecks/behaviors/dsl.rb +58 -0
  27. data/lib/hecks/behaviors/expectations.rb +168 -7
  28. data/lib/hecks/behaviors/ir.rb +11 -0
  29. data/lib/hecks/behaviors/rspec.rb +8 -0
  30. data/lib/hecks/behaviors/runner.rb +19 -0
  31. data/lib/hecks/behaviors.rb +8 -0
  32. data/lib/hecks/bluebook/aggregate.rb +30 -0
  33. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +7 -0
  34. data/lib/hecks/bluebook/assembly/build.rb +25 -4
  35. data/lib/hecks/bluebook/assembly/contract.rb +66 -14
  36. data/lib/hecks/bluebook/assembly/contracts.rb +24 -19
  37. data/lib/hecks/bluebook/assembly/marks.rb +143 -14
  38. data/lib/hecks/bluebook/assembly/specializer.rb +19 -0
  39. data/lib/hecks/bluebook/assembly.rb +25 -7
  40. data/lib/hecks/bluebook/attribute.rb +17 -3
  41. data/lib/hecks/bluebook/behaviour/aggregate.rb +32 -0
  42. data/lib/hecks/bluebook/behaviour/attribute.rb +13 -0
  43. data/lib/hecks/bluebook/behaviour/chapter.rb +55 -2
  44. data/lib/hecks/bluebook/behaviour/command.rb +33 -3
  45. data/lib/hecks/bluebook/behaviour/domain_port.rb +20 -0
  46. data/lib/hecks/bluebook/behaviour/entity.rb +12 -0
  47. data/lib/hecks/bluebook/behaviour/hexagon.rb +26 -0
  48. data/lib/hecks/bluebook/behaviour/lifecycle.rb +23 -2
  49. data/lib/hecks/bluebook/behaviour/policy.rb +34 -9
  50. data/lib/hecks/bluebook/behaviour/process_manager.rb +32 -1
  51. data/lib/hecks/bluebook/behaviour/query.rb +5 -0
  52. data/lib/hecks/bluebook/behaviour/read_model.rb +21 -0
  53. data/lib/hecks/bluebook/behaviour/traits.rb +36 -0
  54. data/lib/hecks/bluebook/behaviour/value_object.rb +17 -5
  55. data/lib/hecks/bluebook/chapter.rb +23 -0
  56. data/lib/hecks/bluebook/command.rb +53 -8
  57. data/lib/hecks/bluebook/domain_port.rb +25 -0
  58. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +196 -37
  59. data/lib/hecks/bluebook/dsl/attribute_collector.rb +79 -42
  60. data/lib/hecks/bluebook/dsl/binding_proxy.rb +59 -0
  61. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +385 -53
  62. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +135 -21
  63. data/lib/hecks/bluebook/dsl/command_builder.rb +189 -27
  64. data/lib/hecks/bluebook/dsl/entity_builder.rb +139 -9
  65. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +41 -0
  66. data/lib/hecks/bluebook/dsl/identity_declaration.rb +23 -6
  67. data/lib/hecks/bluebook/dsl/policy_builder.rb +60 -19
  68. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +79 -15
  69. data/lib/hecks/bluebook/dsl/query_builder.rb +33 -4
  70. data/lib/hecks/bluebook/dsl/read_model_builder.rb +102 -27
  71. data/lib/hecks/bluebook/dsl/rule_reference.rb +60 -8
  72. data/lib/hecks/bluebook/dsl/translation_builder.rb +146 -40
  73. data/lib/hecks/bluebook/dsl/value_object_builder.rb +55 -7
  74. data/lib/hecks/bluebook/entity.rb +29 -0
  75. data/lib/hecks/bluebook/expression/ast_json.rb +115 -23
  76. data/lib/hecks/bluebook/expression/ast_reader.rb +29 -0
  77. data/lib/hecks/bluebook/expression/canonical_form.rb +48 -9
  78. data/lib/hecks/bluebook/expression/evaluator.rb +207 -29
  79. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +36 -0
  80. data/lib/hecks/bluebook/expression/resolver.rb +320 -77
  81. data/lib/hecks/bluebook/hexagon.rb +34 -0
  82. data/lib/hecks/bluebook/lifecycle.rb +11 -0
  83. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -0
  84. data/lib/hecks/bluebook/meta_validator/judge.rb +30 -26
  85. data/lib/hecks/bluebook/meta_validator/plan.rb +43 -8
  86. data/lib/hecks/bluebook/meta_validator/port_judge.rb +1 -0
  87. data/lib/hecks/bluebook/meta_validator/readings.rb +157 -7
  88. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -4
  89. data/lib/hecks/bluebook/meta_validator/shapes.rb +141 -0
  90. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +149 -23
  91. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +5 -4
  92. data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -0
  93. data/lib/hecks/bluebook/meta_validator.rb +180 -84
  94. data/lib/hecks/bluebook/model_check.rb +268 -24
  95. data/lib/hecks/bluebook/pattern_subset.rb +23 -1
  96. data/lib/hecks/bluebook/process_manager.rb +13 -0
  97. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  98. data/lib/hecks/bluebook/project_loader.rb +40 -0
  99. data/lib/hecks/bluebook/project_register.rb +44 -0
  100. data/lib/hecks/bluebook/query.rb +27 -0
  101. data/lib/hecks/bluebook/read_model.rb +21 -1
  102. data/lib/hecks/bluebook/reference.rb +21 -8
  103. data/lib/hecks/bluebook/smoke_test.rb +29 -6
  104. data/lib/hecks/bluebook/synthesizer.rb +34 -0
  105. data/lib/hecks/bluebook/translation.rb +30 -1
  106. data/lib/hecks/bluebook/value_object.rb +23 -5
  107. data/lib/hecks/bluebook.rb +3 -4
  108. data/lib/hecks/codemod.rb +107 -20
  109. data/lib/hecks/construct.rb +15 -1
  110. data/lib/hecks/corpus.rb +146 -25
  111. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  112. data/lib/hecks/doc/reference.rb +185 -16
  113. data/lib/hecks/embryonaut_bluebook.rb +32 -9
  114. data/lib/hecks/facade/handle.rb +76 -3
  115. data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
  116. data/lib/hecks/forms/field_shape.rb +3 -0
  117. data/lib/hecks/forms/page.rb +14 -0
  118. data/lib/hecks/forms/port_argument.rb +12 -0
  119. data/lib/hecks/forms/query_form_renderer.rb +63 -0
  120. data/lib/hecks/forms/record_renderer.rb +58 -0
  121. data/lib/hecks/forms/record_table.rb +27 -0
  122. data/lib/hecks/forms/reference_options.rb +24 -0
  123. data/lib/hecks/forms/value_object_shape.rb +10 -0
  124. data/lib/hecks/fqn.rb +58 -0
  125. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  126. data/lib/hecks/framework/oidc.json +15 -0
  127. data/lib/hecks/framework.rb +43 -20
  128. data/lib/hecks/freezer.rb +17 -1
  129. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
  130. data/lib/hecks/fuzzing/combination_miner.rb +59 -0
  131. data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
  132. data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
  133. data/lib/hecks/fuzzing/differential.rb +34 -0
  134. data/lib/hecks/fuzzing/domain_generator.rb +188 -11
  135. data/lib/hecks/fuzzing/era_boundary.rb +45 -15
  136. data/lib/hecks/fuzzing/form_census.rb +86 -0
  137. data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
  138. data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
  139. data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
  140. data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
  141. data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
  142. data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
  143. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
  144. data/lib/hecks/fuzzing/properties/guards.rb +44 -0
  145. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
  146. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
  147. data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
  148. data/lib/hecks/fuzzing/properties/querying.rb +68 -14
  149. data/lib/hecks/fuzzing/properties.rb +24 -15
  150. data/lib/hecks/fuzzing/qa_settings.rb +12 -0
  151. data/lib/hecks/fuzzing/replay.rb +137 -29
  152. data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
  153. data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
  154. data/lib/hecks/fuzzing/self_consistency.rb +189 -40
  155. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
  156. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
  157. data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
  158. data/lib/hecks/fuzzing/shrinker.rb +114 -2
  159. data/lib/hecks/fuzzing/structural_skips.rb +18 -4
  160. data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
  161. data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
  162. data/lib/hecks/fuzzing/value_generator.rb +98 -10
  163. data/lib/hecks/grammar/evolve.rb +178 -2
  164. data/lib/hecks/grammar.rb +46 -0
  165. data/lib/hecks/ir.rb +38 -7
  166. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  167. data/lib/hecks/literal.rb +32 -0
  168. data/lib/hecks/naming.rb +88 -7
  169. data/lib/hecks/ports/access_control.rb +5 -10
  170. data/lib/hecks/ports/authorization.rb +3 -6
  171. data/lib/hecks/ports/identity_assignment.rb +1 -2
  172. data/lib/hecks/ports/identity_resolution.rb +1 -2
  173. data/lib/hecks/ports/key_vault.port +6 -0
  174. data/lib/hecks/ports/key_vault.rb +58 -0
  175. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
  176. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
  177. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
  178. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
  179. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
  180. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  181. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
  182. data/lib/hecks/ports/query/in_memory.rb +39 -6
  183. data/lib/hecks/ports/query/ordering.rb +15 -0
  184. data/lib/hecks/ports.rb +1 -0
  185. data/lib/hecks/projections/bootstrap_table.rb +43 -8
  186. data/lib/hecks/projections/diagrams.rb +243 -7
  187. data/lib/hecks/projections/glossary/html.rb +88 -0
  188. data/lib/hecks/projections/glossary/markdown.rb +47 -0
  189. data/lib/hecks/projections/glossary/mermaid.rb +48 -0
  190. data/lib/hecks/projections/glossary/sentences.rb +105 -0
  191. data/lib/hecks/projections/glossary.rb +161 -19
  192. data/lib/hecks/projections/model/deviations.rb +44 -0
  193. data/lib/hecks/projections/model.rb +51 -1
  194. data/lib/hecks/projections/oidc.rb +40 -3
  195. data/lib/hecks/projections/parser_table.rb +49 -8
  196. data/lib/hecks/projections/reference.rb +12 -0
  197. data/lib/hecks/projections/rust_vocabulary.rb +219 -16
  198. data/lib/hecks/projections/shape.rb +8 -0
  199. data/lib/hecks/projections/statements.rb +63 -16
  200. data/lib/hecks/projections/vocabulary.rb +17 -0
  201. data/lib/hecks/projector/cli_projector.rb +218 -10
  202. data/lib/hecks/projector/docs_projector.rb +145 -19
  203. data/lib/hecks/projector/exporter.rb +65 -11
  204. data/lib/hecks/projector/ir_projector.rb +6 -0
  205. data/lib/hecks/projector/narrate_projector.rb +136 -15
  206. data/lib/hecks/projector/target.rb +29 -5
  207. data/lib/hecks/projector.rb +74 -6
  208. data/lib/hecks/query_ir.rb +47 -0
  209. data/lib/hecks/query_specification/common/null_policy.rb +5 -3
  210. data/lib/hecks/rendering.rb +6 -0
  211. data/lib/hecks/router/namespace_installer.rb +13 -0
  212. data/lib/hecks/router.rb +55 -0
  213. data/lib/hecks/runtime/aggregate_lock.rb +9 -0
  214. data/lib/hecks/runtime/boot_gates.rb +18 -0
  215. data/lib/hecks/runtime/caller.rb +32 -0
  216. data/lib/hecks/runtime/capability_graph.rb +11 -0
  217. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
  218. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
  219. data/lib/hecks/runtime/command_interpreter.rb +42 -17
  220. data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
  221. data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
  222. data/lib/hecks/runtime/command_rules/references.rb +118 -28
  223. data/lib/hecks/runtime/dependency_planning.rb +45 -0
  224. data/lib/hecks/runtime/dispatcher.rb +28 -50
  225. data/lib/hecks/runtime/entity_element.rb +161 -8
  226. data/lib/hecks/runtime/entity_interpreter.rb +44 -9
  227. data/lib/hecks/runtime/errors.rb +18 -4
  228. data/lib/hecks/runtime/event.rb +10 -5
  229. data/lib/hecks/runtime/identity.rb +71 -3
  230. data/lib/hecks/runtime/instance.rb +67 -7
  231. data/lib/hecks/runtime/interpreting.rb +13 -5
  232. data/lib/hecks/runtime/invocation.rb +118 -36
  233. data/lib/hecks/runtime/loader.rb +94 -8
  234. data/lib/hecks/runtime/outbox.rb +145 -7
  235. data/lib/hecks/runtime/policy_interpreter.rb +22 -9
  236. data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
  237. data/lib/hecks/runtime/query_interpreter.rb +40 -12
  238. data/lib/hecks/runtime/reaction_invocation.rb +53 -8
  239. data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
  240. data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
  241. data/lib/hecks/runtime/reference_hop.rb +42 -0
  242. data/lib/hecks/runtime/refusal_wording.rb +50 -0
  243. data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
  244. data/lib/hecks/runtime/registry/verification.rb +119 -4
  245. data/lib/hecks/runtime/registry.rb +157 -4
  246. data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
  247. data/lib/hecks/runtime/routing.rb +27 -2
  248. data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
  249. data/lib/hecks/runtime/saga_interpreter.rb +27 -13
  250. data/lib/hecks/runtime/tenant_check.rb +26 -6
  251. data/lib/hecks/runtime/tenant_scope.rb +18 -0
  252. data/lib/hecks/runtime/value/coercion.rb +255 -33
  253. data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
  254. data/lib/hecks/runtime/value.rb +50 -0
  255. data/lib/hecks/runtime.rb +32 -0
  256. data/lib/hecks/storehouse.rb +305 -9
  257. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  258. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  259. data/lib/hecks/version.rb +1 -1
  260. data/lib/hecks.rb +79 -1
  261. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
  262. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
  263. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
  264. metadata +11 -5
  265. data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
  266. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
  267. data/lib/hecks/deprecation.rb +0 -95
@@ -188,10 +188,10 @@ module Hecks
188
188
  # storehouse-kernel files, byte-identical text) -- `.start_with?(`/
189
189
  # `.end_with?(` matched none of this grammar's known suffixes, so
190
190
  # both fell through to the `Lookup` catch-all and crashed with the
191
- # identical `TypeError: no implicit conversion of Symbol into
192
- # Integer` shape `.split`/`.all?` used to, confirmed live via a
193
- # real dispatch (not validate), not inferred. Two separate node
194
- # 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`/
195
195
  # `SignTest` precedent) -- `start_with?`/`end_with?` aren't two
196
196
  # spellings of the same test the way `all?`/`any?`/`none?` are (one
197
197
  # Array-aggregation family) or `positive?`/`negative?`/`zero?` are
@@ -206,6 +206,23 @@ module Hecks
206
206
 
207
207
  module_function
208
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
209
226
  def resolve(expr, state, attrs)
210
227
  interpret(parse(expr), state, attrs)
211
228
  end
@@ -218,6 +235,15 @@ module Hecks
218
235
  # branch is already one line; splitting the table into smaller
219
236
  # methods would not shrink any single check, only hide the
220
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
221
247
  # rubocop:disable-next Metrics/AbcSize
222
248
  # rubocop:disable-next Metrics/CyclomaticComplexity
223
249
  # rubocop:disable-next Metrics/MethodLength
@@ -287,6 +313,13 @@ module Hecks
287
313
  Lookup.new(path: expr)
288
314
  end
289
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
290
323
  def sign_test_node(parts)
291
324
  receiver, test = parts
292
325
  symbol = SIGN_TEST_OPERATORS.fetch(test)
@@ -301,6 +334,21 @@ module Hecks
301
334
  # exhaust; the `else` backstop's own comment explains why a
302
335
  # missing arm is a bug this method is built to make loud, not
303
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
304
352
  # rubocop:disable-next Metrics/AbcSize
305
353
  # rubocop:disable-next Metrics/CyclomaticComplexity
306
354
  # rubocop:disable-next Metrics/MethodLength
@@ -368,6 +416,11 @@ module Hecks
368
416
  # `Value#to_h`'d first) is present regardless of what its own
369
417
  # inner value holds, matching how every VO-typed field in this
370
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
371
424
  def blank?(value)
372
425
  return true if value.nil? || value == false
373
426
 
@@ -392,6 +445,18 @@ module Hecks
392
445
  # `Modulo`'s own parse rule above calling a same-named
393
446
  # `match_call` that has the identical cross-module problem --
394
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
395
460
  def matches_regex?(receiver_value, pattern, flags)
396
461
  text = case receiver_value
397
462
  when String, Symbol, Integer, Float then receiver_value.to_s
@@ -421,6 +486,11 @@ module Hecks
421
486
  # Splits on top-level commas only — quote-aware and depth-aware,
422
487
  # the same discipline `split_addition` already applies, so a
423
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
424
494
  def array_elements(expr)
425
495
  return nil unless expr.start_with?("[") && expr.end_with?("]")
426
496
 
@@ -466,6 +536,11 @@ module Hecks
466
536
  # downstream chess domain's castling given; the evaluator's own
467
537
  # top_level_index has counted braces since its own version of this
468
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
469
544
  def split_addition(expr)
470
545
  depth = 0
471
546
  quote = nil
@@ -479,12 +554,13 @@ module Hecks
479
554
  # comment already names, a third time (found live via the
480
555
  # type-directed bounded-exhaustive expression generator,
481
556
  # Phase 7 of the equivalence-gap plan): `ArrayLiteral` can
482
- # appear as a general sub-expression now, not only as
483
- # `.include?`'s own haystack, so an array element containing
484
- # its own top-level `+` (`[0, 0 + 0]`) used to read as this
485
- # expression's own addition split point -- the whole
486
- # receiver before `.all?`/`.any?`/etc. torn in half before
487
- # `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.
488
564
  elsif ["(", "{", "["].include?(char)
489
565
  depth += 1
490
566
  elsif [")", "}", "]"].include?(char)
@@ -503,6 +579,16 @@ module Hecks
503
579
  # it. A Float sum that is not finite is the same fault (C3.4).
504
580
  INT64_RANGE = (-(2**63))..((2**63) - 1)
505
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
506
592
  def add(left, right)
507
593
  lhs = require_number(left, "addition")
508
594
  rhs = require_number(right, "addition")
@@ -517,6 +603,12 @@ module Hecks
517
603
  raise EvaluationError, "addition overflowed: #{lhs} + #{rhs} is not a finite number"
518
604
  end
519
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
520
612
  def quoted?(expr)
521
613
  return false if expr.length < 2
522
614
 
@@ -530,67 +622,111 @@ module Hecks
530
622
  # which admit the same set for the same reason.
531
623
  SIZED_TYPES = Hecks::Vocabulary.fetch("SizedType")
532
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
533
633
  def size_of(value)
534
634
  return value.size if value.is_a?(Array) || value.is_a?(String) || value.is_a?(Hash)
535
635
 
536
636
  raise EvaluationError, "size expects a list or string, got #{describe(value)}"
537
637
  end
538
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
539
647
  def emptiness_of(value)
540
648
  return value.empty? if value.is_a?(Array) || value.is_a?(String) || value.is_a?(Hash)
541
649
 
542
650
  raise EvaluationError, "empty? expects a list or string, got #{describe(value)}"
543
651
  end
544
652
 
545
- # `.split("SEP")` -- vendored addition, see the `Split` struct's
546
- # own comment above. Only a String receiver makes sense to
547
- # split -- unlike `.length`/`.size`/`.empty?`, which are already
548
- # meaningful over Array/Hash too, `.split` is a String-only
549
- # method in the corpus's own usage (every occurrence found this
550
- # 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
551
666
  def split_value(value, separator)
552
667
  raise EvaluationError, "split expects a string, got #{describe(value)}" unless value.is_a?(String)
553
668
 
554
669
  value.split(separator)
555
670
  end
556
671
 
557
- # `.last` -- vendored addition, see the `Last` struct's own
558
- # comment above. Duck-typed on `respond_to?(:last)` rather than
559
- # hard-coding Array -- the one corpus usage found this pass
560
- # (`Query::Phrase`'s `.split("::").last`) always receives a
561
- # `Split`-produced Array, but nothing about `.last` itself is
562
- # Array-specific, and this matches `Empty`/`Size`'s own
563
- # duck-typed-over-a-known-set precedent without inventing a
564
- # 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`
565
685
  def last_of(value)
566
686
  return value.last if value.respond_to?(:last)
567
687
 
568
688
  raise EvaluationError, "last expects a list, got #{describe(value)}"
569
689
  end
570
690
 
571
- # `.first` -- see the `First` struct's own comment above.
572
- # `last_of` with the one method swapped, same duck-typed
573
- # 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`
574
698
  def first_of(value)
575
699
  return value.first if value.respond_to?(:first)
576
700
 
577
701
  raise EvaluationError, "first expects a list, got #{describe(value)}"
578
702
  end
579
703
 
704
+ # Reports whether `value` starts with `substring`, for
580
705
  # `.start_with?("prefix")` -- vendored addition, see the
581
706
  # `StartsWith` struct's own comment above. String-only, same
582
707
  # reasoning as `.split` above -- every corpus usage found this
583
708
  # pass (`Params`'s own JSON-object-shape invariant) receives a
584
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
585
715
  def starts_with?(value, substring)
586
716
  raise EvaluationError, "start_with? expects a string, got #{describe(value)}" unless value.is_a?(String)
587
717
 
588
718
  value.start_with?(substring)
589
719
  end
590
720
 
721
+ # Reports whether `value` ends with `substring`, for
591
722
  # `.end_with?("suffix")` -- vendored addition, see the `EndsWith`
592
723
  # struct's own comment above. Same String-only reasoning as
593
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
594
730
  def ends_with?(value, substring)
595
731
  raise EvaluationError, "end_with? expects a string, got #{describe(value)}" unless value.is_a?(String)
596
732
 
@@ -602,6 +738,13 @@ module Hecks
602
738
  # holds this equal to the language.
603
739
  TO_STRING_TYPES = Hecks::Vocabulary.fetch("ToStringType")
604
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
605
748
  def string_of(value)
606
749
  case value
607
750
  when String then value
@@ -612,6 +755,16 @@ module Hecks
612
755
  end
613
756
  end
614
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
615
768
  def match_suffix(expr, suffixes)
616
769
  suffixes.each do |suffix|
617
770
  marker = ".#{suffix}"
@@ -620,6 +773,14 @@ module Hecks
620
773
  nil
621
774
  end
622
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
623
784
  def apply_sign_test(node, value)
624
785
  number = numeric(value)
625
786
  raise EvaluationError, "#{node.test} expects a number, got #{describe(value)}" unless number
@@ -671,6 +832,14 @@ module Hecks
671
832
  # call to the true final `)`; for chaining, the leftmost
672
833
  # occurrence's close lands short and is rejected, so the next
673
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
674
843
  def match_call(expr, marker)
675
844
  start = 0
676
845
  while (index = expr.index(marker, start))
@@ -682,10 +851,17 @@ module Hecks
682
851
  nil
683
852
  end
684
853
 
685
- # `matching_brace` (resolver/block_predicates.rb)'s own twin, one
686
- # bracket pair over: `start` is the index just past the opening
687
- # `(` already consumed by the caller (depth starts at 1, not 0,
688
- # 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
689
865
  def matching_paren(expr, start)
690
866
  depth = 1
691
867
  quote = nil
@@ -724,6 +900,15 @@ module Hecks
724
900
  # operands down to Integer first was pure data loss with no
725
901
  # purpose: `7.5.modulo(2.5)` silently became `7 % 2` (`1`)
726
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
727
912
  def apply_modulo(receiver_value, divisor_value)
728
913
  receiver = require_number(receiver_value, "modulo")
729
914
  divisor = require_number(divisor_value, "modulo")
@@ -732,6 +917,20 @@ module Hecks
732
917
  receiver % divisor
733
918
  end
734
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
735
934
  def lookup(expr, state, attrs)
736
935
  return unwrap_scalar(fetch(expr, state, attrs)) unless expr.include?(".")
737
936
 
@@ -748,6 +947,16 @@ module Hecks
748
947
  # which spelling answers — a bare `||` between the two would
749
948
  # treat a genuinely-held `false` the same as an absent key and
750
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)
751
960
  def walk_path(value, segments)
752
961
  segments.reduce(value) do |current, segment|
753
962
  break nil unless current.respond_to?(:[])
@@ -764,10 +973,10 @@ module Hecks
764
973
  # or a `list_of` attribute) — Array#[] demands an
765
974
  # Integer/Range and raises a raw TypeError for a String
766
975
  # segment ("no implicit conversion of String into
767
- # Integer"), which used to cross straight past this
768
- # sublanguage's own refusal boundary and crash the
769
- # runtime instead of reading as "this predicate doesn't
770
- # 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.
771
980
  raise EvaluationError,
772
981
  "cannot read #{segment.inspect} from #{describe(current)}"
773
982
  end
@@ -775,6 +984,7 @@ module Hecks
775
984
  end
776
985
  end
777
986
 
987
+ # Unwraps a single-field value object to its own scalar, so
778
988
  # `field == "literal"` -- vendored addition, not (yet) upstream
779
989
  # hecks (migration plan task 8): the third-most pervasive
780
990
  # dispatch-time gap this pass found, same family as `.match?`/
@@ -792,48 +1002,46 @@ module Hecks
792
1002
  # "literal"` shape in plan.bluebook, service_task.bluebook,
793
1003
  # route.bluebook, and subscription.bluebook, all equally silent
794
1004
  # until a real dispatch (never validate) exercised the
795
- # predicate. Scoped narrowly to the single-field `{value: X}`
796
- # shape only.
797
- #
798
- # Update 2026-08-18: originally scoped to unwrap only the bare
799
- # (undotted) case, on the belief that a dotted lookup only ever
800
- # reaches into a VO's own field (`field.value`, `field.sub_
801
- # field`) and so should keep walking `#[]` untouched. That
802
- # belief held for the single-hop case but not for the general
803
- # one: a dotted lookup that navigates through an entity/list
804
- # element to a nested field (`leg.voyage`, where `voyage` is
805
- # itself a single-field VO) landed on the very same unwrapped-
806
- # `Value` shape the bare case fixed, and hit the identical
807
- # silent `Value#==` failure -- comparing it against a raw
808
- # literal or another unwrapped VO returned false for everything,
809
- # no error. The terminal value of a dotted walk deserves the
810
- # same "this VO IS its scalar" treatment as a bare lookup's
811
- # result; only the intermediate hops need raw `#[]` addressing
812
- # to keep navigating. `unwrap_scalar` is idempotent on an
813
- # already-raw scalar (a String/Integer doesn't respond to
814
- # `#to_h`), so this is safe for the existing `field.value`-
815
- # shaped dotted lookups too -- they already returned a raw
816
- # scalar and are unaffected.
817
- # Update (single-element value objects strictly answer `.value`):
818
- # the unwrap used to gate on the sole key being literally named
819
- # `:value` — correct for the shorthand/closed-set shapes that
820
- # motivated it, but a lie of omission for `Money{amount}` and
821
- # every other single-field value object whose author picked a
822
- # domain name for the field: the same "this VO IS its scalar"
823
- # reading ([[feedback_name_the_scalar_field]], `Behaviour::
824
- # ValueObject#sole_attribute`) applies regardless of what the
825
- # sole field happens to be called, and the name gate made a bare
826
- # `balance > 0` work for a `Balance{value}` while silently
827
- # comparing a whole VO for a `Balance{amount}`. Now the count is
828
- # the gate, never the name. A declared `Runtime::Value` reads
829
- # its own `sole_attribute` (the declaration's answer, not the
830
- # stored hash's); any other to_h-able (a Struct, a bespoke
831
- # wrapper with no declaration to consult) keeps the original
832
- # `{value: X}`-only unwrap, so nothing that never was a value
833
- # object gains a surprise unwrapping. `rust/src/kernel/json.rs`'s
834
- # `impl Fielded for Json` mirrors the count-only reading on the
835
- # Rust side — change them in lockstep or rust_conformance
836
- # 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
837
1045
  def unwrap_scalar(value)
838
1046
  return value unless value.respond_to?(:to_h) && !value.is_a?(Hash) && !value.is_a?(Array)
839
1047
 
@@ -846,6 +1054,17 @@ module Hecks
846
1054
  hash.size == 1 && hash.key?(:value) ? hash[:value] : value
847
1055
  end
848
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`
849
1068
  def fetch(name, state, attrs)
850
1069
  key = name.to_sym
851
1070
  return attrs[key] if attrs.key?(key)
@@ -854,21 +1073,45 @@ module Hecks
854
1073
  raise EvaluationError, "cannot resolve #{name.inspect} — no such attribute or argument"
855
1074
  end
856
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
857
1084
  def known?(state, key)
858
1085
  return state.key?(key) if state.respond_to?(:key?)
859
1086
 
860
1087
  !state[key].nil?
861
1088
  end
862
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`
863
1095
  def numeric(value)
864
1096
  value if value.is_a?(Integer) || value.is_a?(Float)
865
1097
  end
866
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
867
1106
  def require_number(value, operation)
868
1107
  numeric(value) ||
869
1108
  raise(EvaluationError, "#{operation} expects a number, got #{describe(value)}")
870
1109
  end
871
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)`
872
1115
  def describe(value) = Rendering.describe(value)
873
1116
  end
874
1117
  end