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
@@ -53,14 +53,31 @@ module Hecks
53
53
 
54
54
  module_function
55
55
 
56
- # Keyed by the exact string `call` receives. Canonical text is already
56
+ # Memoizes each distinct predicate string's own parsed AST, keyed by
57
+ # the exact string `call` receives. Canonical text is already
57
58
  # normalised at DSL-build time, so the same given/invariant's text is
58
59
  # byte-identical across every dispatch that evaluates it — parsed once
59
60
  # here, interpreted fresh against each call's own state/attrs. Matches
60
61
  # MetaValidator.verdicts' unsynchronized `||= {}` idiom : redundant
61
62
  # parse work under real parallelism, never corruption.
63
+ #
64
+ # @return [Hash{String => Object}] the process-wide parse cache,
65
+ # keyed by predicate string; each value is one of `Or`, `And`,
66
+ # `Not`, `Compare`, `Include`, or `Resolve`
62
67
  def ast_cache = @ast_cache ||= {}
63
68
 
69
+ # Parses `expr` (cached per distinct string) and interprets it
70
+ # against `state`/`attrs`.
71
+ #
72
+ # @param expr [String] the canonical predicate text to evaluate
73
+ # @param state [Hash{Symbol => Object}] the stored attribute values
74
+ # a `Resolve`/`Compare` leaf may resolve against
75
+ # @param attrs [Hash{Symbol => Object}] the call's own argument
76
+ # values, checked before `state`
77
+ # @return [Boolean] whether `expr` holds
78
+ # @raise [EvaluationError] if `expr` resolves an unknown attribute
79
+ # or argument, or applies an operation to a value of the wrong
80
+ # type
64
81
  def call(expr, state, attrs = {})
65
82
  interpret(ast_cache[expr] ||= parse(expr), state, attrs)
66
83
  end
@@ -77,31 +94,60 @@ module Hecks
77
94
  #
78
95
  # HECKS_EVAL=string reverts to the text path wholesale, kept for
79
96
  # one release as the escape hatch while the ast path beds in.
97
+ #
98
+ # @param rule [Bluebook::Given, Bluebook::Invariant] the rule to
99
+ # evaluate
100
+ # @param state [Hash{Symbol => Object}] the stored attribute values
101
+ # a `Resolve`/`Compare` leaf may resolve against
102
+ # @param attrs [Hash{Symbol => Object}] the call's own argument
103
+ # values, checked before `state`
104
+ # @return [Boolean] whether `rule` holds
105
+ # @raise [EvaluationError] if `rule` resolves an unknown attribute
106
+ # or argument, or applies an operation to a value of the wrong
107
+ # type
80
108
  def call_rule(rule, state, attrs = {})
81
109
  return call(rule.canonical, state, attrs) if ENV["HECKS_EVAL"] == "string"
82
110
 
83
111
  interpret(ast_cache[rule.canonical] ||= nodes_for(rule), state, attrs)
84
112
  end
85
113
 
114
+ # Returns `rule`'s own AST, read back from its `ast` field when
115
+ # present, otherwise parsed fresh from `canonical`.
116
+ #
117
+ # @param rule [Bluebook::Given, Bluebook::Invariant] the rule to
118
+ # read
119
+ # @return [Object] one of `Or`, `And`, `Not`, `Compare`, `Include`,
120
+ # or `Resolve`
86
121
  def nodes_for(rule)
87
122
  rule.ast ? AstReader.read_predicate(rule.ast) : parse(rule.canonical)
88
123
  end
89
124
 
90
- # A refused `given`/`ensures`/`invariant` names its own description
91
- # ("not already superseded") but, on its own, not what the block
92
- # actually evaluated to — the difference between "the rule is right
93
- # and my data is wrong" and "the rule is subtly wrong" is often just
94
- # seeing the two operands. Scoped to the single shape that has one
95
- # honest answer: `expr`'s own top-level node is a bare `Compare` —
96
- # not `Or`/`And`/`Not` (which of several sub-comparisons would even
97
- # be "the" one at fault is genuinely ambiguous), `Include` (no
98
- # left/right to show), or `Resolve` (a bare boolean read, nothing to
99
- # compare against). Values are rendered with `Rendering.describe`,
100
- # the same house style every other refusal already prints a value
101
- # through. Returns `nil` — not raised — on anything else, including
102
- # an operand that itself fails to resolve (`EvaluationError`): a
125
+ # Renders `expr`'s own two operands, for a refusal message to show
126
+ # alongside a failed `given`/`ensures`/`invariant`. A refused rule
127
+ # names its own description ("not already superseded") but, on its
128
+ # own, not what the block actually evaluated to — the difference
129
+ # between "the rule is right and my data is wrong" and "the rule
130
+ # is subtly wrong" is often just seeing the two operands. Scoped
131
+ # to the single shape that has one honest answer: `expr`'s own
132
+ # top-level node is a bare `Compare` — not `Or`/`And`/`Not` (which
133
+ # of several sub-comparisons would even be "the" one at fault is
134
+ # genuinely ambiguous), `Include` (no left/right to show), or
135
+ # `Resolve` (a bare boolean read, nothing to compare against).
136
+ # Values are rendered with `Rendering.describe`, the same house
137
+ # style every other refusal already prints a value through.
138
+ # Returns `nil` — not raised — on anything else, including an
139
+ # operand that itself fails to resolve (`EvaluationError`): a
103
140
  # missing diagnostic is a worse debugging experience than none, a
104
141
  # crash while building one is worse still.
142
+ #
143
+ # @param expr [String] the canonical predicate text that was just
144
+ # refused
145
+ # @param state [Hash{Symbol => Object}] the stored attribute values
146
+ # the operands may resolve against
147
+ # @param attrs [Hash{Symbol => Object}] the call's own argument
148
+ # values, checked before `state`
149
+ # @return [String, nil] `"left: X, right: Y"` when `expr` is a bare
150
+ # comparison whose operands both resolve; `nil` otherwise
105
151
  def comparison_detail(expr, state, attrs = {})
106
152
  node = ast_cache[expr] ||= parse(expr)
107
153
  return nil unless node.is_a?(Compare)
@@ -113,6 +159,12 @@ module Hecks
113
159
  nil
114
160
  end
115
161
 
162
+ # Parses `expr`'s boolean/comparison grammar into an AST, recursing
163
+ # into `Resolver.parse` for each leaf.
164
+ #
165
+ # @param expr [String] the canonical predicate text to parse
166
+ # @return [Object] one of `Or`, `And`, `Not`, `Compare`, `Include`,
167
+ # or `Resolve`, chosen by `expr`'s own shape
116
168
  def parse(expr)
117
169
  expr = strip_parens(expr.to_s.strip)
118
170
 
@@ -127,13 +179,13 @@ module Hecks
127
179
  # means `!(names.include?(x))`, never "call .include? on the negated
128
180
  # receiver"), so the leading marker has to be stripped and the
129
181
  # remainder re-parsed before anything downstream gets a chance to
130
- # mis-scan across it. It used to sit after `match_include`, whose
182
+ # mis-scan across it. Tried after `match_include` instead, its
131
183
  # naive `rindex(".include?(")` has no concept of a leading `!` —
132
- # for `!names.include?(x)` it swallowed the `!` straight into the
133
- # haystack text ("!names"), which `Resolver.parse` cannot resolve,
134
- # so every spelling of negated membership raised instead of
135
- # evaluating. Moving the check here fixes both the bare prefix
136
- # (`!names.include?(x)`) and the parenthesized form
184
+ # for `!names.include?(x)` it would swallow the `!` straight into
185
+ # the haystack text ("!names"), which `Resolver.parse` cannot
186
+ # resolve, so every spelling of negated membership would raise
187
+ # instead of evaluating. Checking here first fixes both the bare
188
+ # prefix (`!names.include?(x)`) and the parenthesized form
137
189
  # (`!(names.include?(x))`) — the recursive `parse` call sees the
138
190
  # clean remainder and correctly finds the `.include?` (or `&&`/`||`)
139
191
  # inside it.
@@ -150,6 +202,19 @@ module Hecks
150
202
  Resolve.new(expr: Resolver.parse(expr))
151
203
  end
152
204
 
205
+ # Interprets a parsed boolean/comparison node against `state`/
206
+ # `attrs`.
207
+ #
208
+ # @param node [Object] a node `parse` produced (`Or`, `And`, `Not`,
209
+ # `Compare`, `Include`, or `Resolve`)
210
+ # @param state [Hash{Symbol => Object}] the stored attribute values
211
+ # a leaf may resolve against
212
+ # @param attrs [Hash{Symbol => Object}] the call's own argument
213
+ # values, checked before `state`
214
+ # @return [Boolean] whether `node` holds
215
+ # @raise [EvaluationError] if `node` is not one of the handled
216
+ # types, or a leaf it delegates to (`compare`, `includes?`,
217
+ # `Resolver.interpret`) refuses its operand
153
218
  def interpret(node, state, attrs)
154
219
  case node
155
220
  when Or then interpret(node.left, state, attrs) || interpret(node.right, state, attrs)
@@ -162,16 +227,30 @@ module Hecks
162
227
  # Every node `parse` can produce has a `when` above — a
163
228
  # backstop against the day this grammar grows a new node
164
229
  # type and `interpret` doesn't grow to match it. A missing
165
- # arm here used to return bare `nil`, and `Or`/`And` fold
166
- # that straight into the boolean algebra as ordinary falsy
167
- # — reading exactly like "the rule legitimately does not
168
- # hold" rather than "the runtime cannot evaluate this rule
169
- # at all", the one silent no-op this language otherwise
230
+ # arm here would instead return bare `nil`, and `Or`/`And`
231
+ # fold that straight into the boolean algebra as ordinary
232
+ # falsy — reading exactly like "the rule legitimately does
233
+ # not hold" rather than "the runtime cannot evaluate this
234
+ # rule at all", the one silent no-op this language otherwise
170
235
  # refuses.
171
236
  raise EvaluationError, "no interpreter handles #{node.class} — add a case before parse can produce it"
172
237
  end
173
238
  end
174
239
 
240
+ # Resolves `left`/`right` and applies `comparator` to the results.
241
+ #
242
+ # @param comparator [Operator] the comparison operator to apply
243
+ # @param left [Object] a `Resolver` leaf node (`Resolver.parse`'s
244
+ # own return) for the left operand
245
+ # @param right [Object] a `Resolver` leaf node (`Resolver.parse`'s
246
+ # own return) for the right operand
247
+ # @param state [Hash{Symbol => Object}] the stored attribute values
248
+ # the operands may resolve against
249
+ # @param attrs [Hash{Symbol => Object}] the call's own argument
250
+ # values, checked before `state`
251
+ # @return [Boolean] the comparison's result
252
+ # @raise [EvaluationError] if an operand does not resolve, or
253
+ # `less_than`/`equal?` cannot compare the resolved values
175
254
  def compare(comparator, left, right, state, attrs)
176
255
  lhs = Resolver.interpret(left, state, attrs)
177
256
  rhs = Resolver.interpret(right, state, attrs)
@@ -183,12 +262,27 @@ module Hecks
183
262
  # test (SignTest#compares_via names an Operator symbol) can apply the
184
263
  # same primitives compare() uses against the literal 0, rather than
185
264
  # re-deriving positive?/negative?/zero? by hand a second time.
265
+ #
266
+ # @param comparator [Operator] the comparison operator to apply
267
+ # @param lhs [Object] the already-resolved left operand
268
+ # @param rhs [Object] the already-resolved right operand
269
+ # @return [Boolean] `comparator`'s result over `lhs`/`rhs`
270
+ # @raise [EvaluationError] if `comparator` tests less-than and
271
+ # `lhs`/`rhs` are not both numeric or both String
186
272
  def apply(comparator, lhs, rhs)
187
273
  result = (comparator.compares_less_than && less_than(lhs, rhs)) ||
188
274
  (comparator.compares_equal && equal?(lhs, rhs))
189
275
  comparator.negated ? !result : result
190
276
  end
191
277
 
278
+ # Compares two already-resolved operands for the `<` primitive.
279
+ #
280
+ # @param lhs [Object] the already-resolved left operand
281
+ # @param rhs [Object] the already-resolved right operand
282
+ # @return [Boolean] whether `lhs` is less than `rhs`, comparing
283
+ # numerically if both are numeric, lexically if both are String
284
+ # @raise [EvaluationError] if `lhs`/`rhs` are not both numeric or
285
+ # both String
192
286
  def less_than(lhs, rhs)
193
287
  left = Resolver.numeric(lhs)
194
288
  right = Resolver.numeric(rhs)
@@ -199,6 +293,12 @@ module Hecks
199
293
  "comparison of #{class_of(lhs)} with #{Resolver.describe(rhs)} failed"
200
294
  end
201
295
 
296
+ # Compares two already-resolved operands for the `equal` primitive.
297
+ #
298
+ # @param lhs [Object] the already-resolved left operand
299
+ # @param rhs [Object] the already-resolved right operand
300
+ # @return [Boolean] whether `lhs` equals `rhs`, comparing
301
+ # numerically if both are numeric, `==` otherwise
202
302
  def equal?(lhs, rhs)
203
303
  left = Resolver.numeric(lhs)
204
304
  right = Resolver.numeric(rhs)
@@ -207,10 +307,19 @@ module Hecks
207
307
  lhs == rhs
208
308
  end
209
309
 
310
+ # Reports whether `value` is truthy, for a bare `Resolve` node.
311
+ #
312
+ # @param value [Object] the value to test
313
+ # @return [Boolean] Ruby's own truthiness: `false` for `nil` and
314
+ # `false`, `true` for everything else
210
315
  def truthy?(value)
211
316
  !value.nil? && value != false
212
317
  end
213
318
 
319
+ # Names `value`'s class, for a refusal message.
320
+ #
321
+ # @param value [Object] the value to name
322
+ # @return [String] `"nil"` for `nil`, otherwise `value.class.name`
214
323
  def class_of(value)
215
324
  value.nil? ? "nil" : value.class.name
216
325
  end
@@ -227,6 +336,13 @@ module Hecks
227
336
  # string's last character — `Resolver.matching_paren` is reused
228
337
  # directly rather than duplicated, the same depth-tracking rule
229
338
  # either grammar layer needs here.
339
+ # Splits `expr` at its outermost `.include?(...)` call, if any.
340
+ #
341
+ # @param expr [String] the boolean-position expression text
342
+ # @return [Array(String, String), nil] the `[haystack_text,
343
+ # needle_text]` pair for the outermost `.include?(` occurrence
344
+ # whose matching close paren reaches `expr`'s last character, or
345
+ # `nil` if none does
230
346
  def match_include(expr)
231
347
  start = 0
232
348
  marker = ".include?("
@@ -246,6 +362,19 @@ module Hecks
246
362
  # does.
247
363
  INCLUDE_HAYSTACKS = Hecks::Vocabulary.fetch("IncludeHaystack")
248
364
 
365
+ # Resolves and evaluates an `Include` node's own `.include?` test.
366
+ #
367
+ # @param parts [Array(Object, Object)] the `[haystack, needle]`
368
+ # pair of `Resolver` leaf nodes from `Include#haystack`/`#needle`
369
+ # @param state [Hash{Symbol => Object}] the stored attribute values
370
+ # the operands may resolve against
371
+ # @param attrs [Hash{Symbol => Object}] the call's own argument
372
+ # values, checked before `state`
373
+ # @return [Boolean] whether the resolved haystack includes the
374
+ # resolved needle (`equal?`-compared for an Array, `#include?`
375
+ # for a String); `false` for any other resolved haystack type
376
+ # @raise [EvaluationError] if the haystack resolves to a String and
377
+ # the needle does not
249
378
  def includes?(parts, state, attrs)
250
379
  haystack, needle = parts
251
380
  wanted = Resolver.interpret(needle, state, attrs)
@@ -260,6 +389,11 @@ module Hecks
260
389
  end
261
390
  end
262
391
 
392
+ # Strips a redundant outer pair of parens, recursively.
393
+ #
394
+ # @param expr [String] the expression text
395
+ # @return [String] `expr` with every redundant outer `(...)` pair
396
+ # removed; `expr` unchanged if it is not wholly parenthesized
263
397
  def strip_parens(expr)
264
398
  return expr unless expr.start_with?("(") && expr.end_with?(")")
265
399
 
@@ -272,6 +406,14 @@ module Hecks
272
406
  strip_parens(expr[1..-2].strip)
273
407
  end
274
408
 
409
+ # Splits `expr` at its first top-level occurrence of `operator`.
410
+ #
411
+ # @param expr [String] the expression text
412
+ # @param operator [String] the operator text to split on, such as
413
+ # `"||"` or `"&&"`
414
+ # @return [Array(String, String), nil] the `[left, right]` operand
415
+ # text around the split, or `nil` if `expr` has no top-level
416
+ # occurrence
275
417
  def split_top_level(expr, operator)
276
418
  index = top_level_index(expr, operator)
277
419
  return nil unless index
@@ -279,6 +421,17 @@ module Hecks
279
421
  [expr[0...index].strip, expr[(index + operator.length)..].strip]
280
422
  end
281
423
 
424
+ # Splits `expr` at its first top-level occurrence of `operator`
425
+ # that is not part of a longer operator's own spelling (`==` is
426
+ # not mistaken for the middle of `===`, `<` is not mistaken for
427
+ # the leading `<` of `<=`).
428
+ #
429
+ # @param expr [String] the expression text
430
+ # @param operator [String] the comparison operator text to split
431
+ # on, such as `"=="` or `"<"`
432
+ # @return [Array(String, String), nil] the `[left, right]` operand
433
+ # text around the split, or `nil` if `expr` has no matching
434
+ # top-level occurrence
282
435
  def split_comparison(expr, operator)
283
436
  index = top_level_index(expr, operator) { |at| !part_of_longer?(expr, at, operator) }
284
437
  return nil unless index
@@ -286,6 +439,15 @@ module Hecks
286
439
  [expr[0...index].strip, expr[(index + operator.length)..].strip]
287
440
  end
288
441
 
442
+ # Reports whether the occurrence of `operator` at `index` is
443
+ # actually the middle of a longer operator's own spelling (`==`
444
+ # inside `===`, or `<`/`>`/`!`/`=` immediately before a bare `=`).
445
+ #
446
+ # @param expr [String] the expression text
447
+ # @param index [Integer] the index of the candidate occurrence
448
+ # @param operator [String] the operator text being tried
449
+ # @return [Boolean] whether this occurrence belongs to a longer
450
+ # operator and should be skipped
289
451
  def part_of_longer?(expr, index, operator)
290
452
  after = expr[index + operator.length]
291
453
  before = index.positive? ? expr[index - 1] : nil
@@ -296,6 +458,21 @@ module Hecks
296
458
  false
297
459
  end
298
460
 
461
+ # A grammar's own depth-aware scanner, shared by `split_top_level`/
462
+ # `split_comparison` — one pass finds the first top-level
463
+ # occurrence of `operator`, tracking quotes and every bracket
464
+ # kind (`(`/`)`, `{`/`}`, `[`/`]`) so an operator inside a nested
465
+ # call, block predicate, or array literal is never mistaken for a
466
+ # split point at this level.
467
+ #
468
+ # @param expr [String] the expression text
469
+ # @param operator [String] the operator text to search for
470
+ # @yieldparam index [Integer] a candidate top-level occurrence's
471
+ # index, offered so the caller can reject it (`split_comparison`
472
+ # uses this to skip a longer operator's own spelling)
473
+ # @yieldreturn [Boolean] whether to accept this occurrence
474
+ # @return [Integer, nil] the accepted occurrence's index, or `nil`
475
+ # if `operator` has no top-level occurrence the block accepts
299
476
  def top_level_index(expr, operator)
300
477
  depth = 0
301
478
  quote = nil
@@ -335,10 +512,11 @@ module Hecks
335
512
  # (`[a, b]`) can appear as a general sub-expression, not only
336
513
  # as `.include?`'s own haystack, the moment an array-typed
337
514
  # attribute or a synthesized literal is embedded anywhere else
338
- # -- and an element containing a top-level `+`/comparison of
339
- # its own (`[0, 0 + 0]`) used to read as a split point for
340
- # this expression's own boolean/comparison grammar, exactly
341
- # the way an un-tracked `{`/`}` once did for block predicates.
515
+ # -- so without counting `[`/`]` toward depth here too, an
516
+ # element containing a top-level `+`/comparison of its own
517
+ # (`[0, 0 + 0]`) reads as a split point for this expression's
518
+ # own boolean/comparison grammar, exactly the way an
519
+ # un-tracked `{`/`}` does for block predicates.
342
520
  elsif ["(", "{", "["].include?(char)
343
521
  depth += 1
344
522
  elsif [")", "}", "]"].include?(char)
@@ -119,6 +119,10 @@ module Hecks
119
119
  # is captured from whatever follows the closing brace ; every
120
120
  # other suffix instead requires nothing follow it at all (the
121
121
  # `BlockPredicate` shape, unchanged from before this rewrite).
122
+ #
123
+ # @param expr [String] the leaf expression text to parse
124
+ # @return [Find, BlockPredicate, nil] the parsed node, or nil when `expr` does
125
+ # not open a `.all?`/`.any?`/`.none?`/`.find` block, or its brace never closes
122
126
  def parse_block_opener(expr)
123
127
  pattern = /\A(.+?)\.(#{BLOCK_OPENER_SUFFIXES.map { |suffix| Regexp.escape(suffix) }.join('|')})\s*\{\s*\|(\w+)\|\s*/m
124
128
  header = expr.match(pattern)
@@ -155,6 +159,11 @@ module Hecks
155
159
  # quoted substring (`.start_with?("}")`) never miscounts, the
156
160
  # same discipline `split_addition`/`array_elements` already
157
161
  # apply for their own depth tracking.
162
+ #
163
+ # @param expr [String] the text to scan
164
+ # @param start [Integer] the index just after the opening `{`, where depth is 1
165
+ # @return [Integer, nil] the index of the matching `}`, or nil if `expr` runs
166
+ # out before depth returns to 0
158
167
  def matching_brace(expr, start)
159
168
  depth = 1
160
169
  quote = nil
@@ -186,6 +195,13 @@ module Hecks
186
195
  # variable concept added anywhere else in Resolver's state model,
187
196
  # just `attrs` extended with the bound name for the span of that
188
197
  # one predicate evaluation, discarded immediately after.
198
+ #
199
+ # @param node [BlockPredicate] the parsed `.all?`/`.any?`/`.none?` node
200
+ # @param collection [Object] the interpreted receiver, expected to be an Array
201
+ # @param state [Hash{Symbol => Object}] the record's own current state
202
+ # @param attrs [Hash{Symbol => Object}] the command's own bound arguments
203
+ # @return [Boolean] whether the collection satisfies `node.mode`
204
+ # @raise [EvaluationError] if `collection` is not an Array
189
205
  def evaluate_block_predicate(node, collection, state, attrs)
190
206
  raise EvaluationError, "#{node.mode}? expects a list, got #{describe(collection)}" unless collection.is_a?(Array)
191
207
 
@@ -202,6 +218,18 @@ module Hecks
202
218
  # evaluation -- `attrs` wins over `state` in `fetch` (see below),
203
219
  # so the bound name shadows any same-named state/attrs field for
204
220
  # the span of this one call only ; nothing persists past it.
221
+ #
222
+ # @param node [BlockPredicate, Find] the node whose `param`/
223
+ # `predicate` to bind and interpret
224
+ # @param element [Object] the one collection element to bind
225
+ # `node.param` to
226
+ # @param state [Hash{Symbol => Object}] the record's own current
227
+ # state
228
+ # @param attrs [Hash{Symbol => Object}] the command's own bound
229
+ # arguments
230
+ # @return [Object] `node.predicate` interpreted with `node.param`
231
+ # bound to `element`
232
+ # @raise [EvaluationError] if `node.predicate` refuses to evaluate
205
233
  def interpret_with_element(node, element, state, attrs)
206
234
  Evaluator.interpret(node.predicate, state, attrs.merge(node.param.to_sym => element))
207
235
  end
@@ -219,6 +247,14 @@ module Hecks
219
247
  # value case in this grammar already has, and the one a re-
220
248
  # routing check like "is there a leg after this one" needs :
221
249
  # not finding one is a normal outcome, not an error.
250
+ #
251
+ # @param node [Find] the parsed `.find` node
252
+ # @param collection [Object] the interpreted receiver, expected to be an Array
253
+ # @param state [Hash{Symbol => Object}] the record's own current state
254
+ # @param attrs [Hash{Symbol => Object}] the command's own bound arguments
255
+ # @return [Object, nil] the found element (or `node.path` projected through it),
256
+ # or nil when no element matches or a `path` segment does not resolve
257
+ # @raise [EvaluationError] if `collection` is not an Array
222
258
  def found_of(node, collection, state, attrs)
223
259
  raise EvaluationError, "find expects a list, got #{describe(collection)}" unless collection.is_a?(Array)
224
260