hecks 1.0.2 → 1.2.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 (247) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +3 -3
  3. data/lib/hecks/adapters/driven/d1.rb +48 -28
  4. data/lib/hecks/adapters/driven/folder.rb +10 -3
  5. data/lib/hecks/adapters/driven/google_authentication.rb +2 -4
  6. data/lib/hecks/adapters/driven/heki/journal.rb +1 -1
  7. data/lib/hecks/adapters/driven/heki.rb +3 -5
  8. data/lib/hecks/adapters/driven/in_memory_ordering.rb +4 -1
  9. data/lib/hecks/adapters/driven/lambda.rb +2 -2
  10. data/lib/hecks/adapters/driven/local_storage.adapter +3 -0
  11. data/lib/hecks/adapters/driven/local_storage.rb +120 -0
  12. data/lib/hecks/adapters/driven/memory.rb +48 -3
  13. data/lib/hecks/adapters/driven/postgres/codec.rb +3 -1
  14. data/lib/hecks/adapters/driven/postgres/outbox.rb +100 -0
  15. data/lib/hecks/adapters/driven/postgres/reconnect.rb +57 -0
  16. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +22 -2
  17. data/lib/hecks/adapters/driven/postgres.rb +45 -31
  18. data/lib/hecks/adapters/driven/prism.rb +11 -0
  19. data/lib/hecks/adapters/driven/sql_query_builder.rb +45 -20
  20. data/lib/hecks/adapters/driven/sqlite/codec.rb +3 -1
  21. data/lib/hecks/adapters/driven/sqlite/projection.rb +22 -3
  22. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +23 -1
  23. data/lib/hecks/adapters/driven/sqlite.rb +87 -8
  24. data/lib/hecks/adapters/driven.rb +4 -0
  25. data/lib/hecks/adapters.rb +4 -0
  26. data/lib/hecks/behaviors/dsl.rb +7 -0
  27. data/lib/hecks/behaviors/expectations.rb +32 -4
  28. data/lib/hecks/behaviors/rspec.rb +2 -0
  29. data/lib/hecks/behaviors/runner.rb +4 -0
  30. data/lib/hecks/bluebook/aggregate.rb +6 -5
  31. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +4 -4
  32. data/lib/hecks/bluebook/assembly/build.rb +6 -3
  33. data/lib/hecks/bluebook/assembly/contract.rb +5 -1
  34. data/lib/hecks/bluebook/assembly/contracts.rb +4 -0
  35. data/lib/hecks/bluebook/assembly/marks.rb +15 -4
  36. data/lib/hecks/bluebook/assembly.rb +1 -3
  37. data/lib/hecks/bluebook/attribute.rb +6 -0
  38. data/lib/hecks/bluebook/behaviour/policy.rb +18 -0
  39. data/lib/hecks/bluebook/behaviour/process_manager.rb +17 -1
  40. data/lib/hecks/bluebook/behaviour/read_model.rb +53 -8
  41. data/lib/hecks/bluebook/behaviour/traits.rb +1 -1
  42. data/lib/hecks/bluebook/command.rb +6 -5
  43. data/lib/hecks/bluebook/dsl/adapter_builder.rb +4 -1
  44. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +524 -0
  45. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +29 -477
  46. data/lib/hecks/bluebook/dsl/attribute_collector.rb +14 -6
  47. data/lib/hecks/bluebook/dsl/binding_proxy.rb +5 -0
  48. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +848 -0
  49. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +25 -784
  50. data/lib/hecks/bluebook/dsl/command_builder.rb +53 -13
  51. data/lib/hecks/bluebook/dsl/const_shim.rb +6 -0
  52. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +20 -7
  53. data/lib/hecks/bluebook/dsl/entity_builder.rb +27 -6
  54. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +12 -2
  55. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +8 -1
  56. data/lib/hecks/bluebook/dsl/identity_declaration.rb +24 -12
  57. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +40 -1
  58. data/lib/hecks/bluebook/dsl/policy_builder.rb +10 -1
  59. data/lib/hecks/bluebook/dsl/port_builder.rb +6 -1
  60. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +33 -12
  61. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +56 -12
  62. data/lib/hecks/bluebook/dsl/query_builder.rb +7 -1
  63. data/lib/hecks/bluebook/dsl/read_model_builder.rb +142 -19
  64. data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -1
  65. data/lib/hecks/bluebook/dsl/translation_builder.rb +13 -2
  66. data/lib/hecks/bluebook/dsl/value_object_builder.rb +8 -1
  67. data/lib/hecks/bluebook/dsl/word_gate.rb +5 -0
  68. data/lib/hecks/bluebook/dsl/world_builder.rb +13 -3
  69. data/lib/hecks/bluebook/entity.rb +5 -3
  70. data/lib/hecks/bluebook/expression/ast_json.rb +88 -9
  71. data/lib/hecks/bluebook/expression/ast_reader.rb +107 -0
  72. data/lib/hecks/bluebook/expression/canonical_form.rb +7 -0
  73. data/lib/hecks/bluebook/expression/evaluator.rb +64 -11
  74. data/lib/hecks/bluebook/expression/projection.json +12 -0
  75. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +4 -3
  76. data/lib/hecks/bluebook/expression/resolver.rb +83 -11
  77. data/lib/hecks/bluebook/expression.rb +1 -0
  78. data/lib/hecks/bluebook/hexagon.rb +10 -0
  79. data/lib/hecks/bluebook/lifecycle.rb +9 -0
  80. data/lib/hecks/bluebook/meta_validator/judge.rb +10 -4
  81. data/lib/hecks/bluebook/meta_validator/plan.rb +4 -0
  82. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +1 -1
  83. data/lib/hecks/bluebook/meta_validator/shapes.rb +3 -3
  84. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +7 -4
  85. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +42 -3
  86. data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -1
  87. data/lib/hecks/bluebook/meta_validator.rb +5 -5
  88. data/lib/hecks/bluebook/model_check.rb +119 -82
  89. data/lib/hecks/bluebook/pattern_subset.rb +9 -0
  90. data/lib/hecks/bluebook/policy.rb +2 -1
  91. data/lib/hecks/bluebook/process_manager.rb +6 -0
  92. data/lib/hecks/bluebook/project_register.rb +45 -25
  93. data/lib/hecks/bluebook/query.rb +4 -0
  94. data/lib/hecks/bluebook/read_model.rb +2 -2
  95. data/lib/hecks/bluebook/smoke_test.rb +5 -0
  96. data/lib/hecks/bluebook/synthesizer.rb +2 -1
  97. data/lib/hecks/bluebook/translation.rb +3 -0
  98. data/lib/hecks/bluebook/value_object.rb +2 -2
  99. data/lib/hecks/codemod.rb +13 -0
  100. data/lib/hecks/doc/reference.rb +17 -11
  101. data/lib/hecks/facade/cli_runner.rb +2 -2
  102. data/lib/hecks/facade/command_request.rb +11 -11
  103. data/lib/hecks/facade/handle.rb +3 -3
  104. data/lib/hecks/facade/surface/aggregate_door.rb +52 -38
  105. data/lib/hecks/facade.rb +4 -0
  106. data/lib/hecks/forms/app.rb +10 -6
  107. data/lib/hecks/forms/command_form_renderer.rb +3 -3
  108. data/lib/hecks/forms/field_renderer.rb +14 -9
  109. data/lib/hecks/forms/field_shape.rb +7 -2
  110. data/lib/hecks/forms/html.rb +5 -2
  111. data/lib/hecks/forms/index_renderer.rb +1 -1
  112. data/lib/hecks/forms/page.rb +7 -3
  113. data/lib/hecks/forms/query_form_renderer.rb +3 -2
  114. data/lib/hecks/forms/record_renderer.rb +22 -16
  115. data/lib/hecks/forms.rb +4 -0
  116. data/lib/hecks/fqn.rb +21 -9
  117. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +30 -8
  118. data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -1
  119. data/lib/hecks/fuzzing/isolated_boot.rb +14 -6
  120. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +264 -0
  121. data/lib/hecks/fuzzing/properties/guards.rb +251 -0
  122. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +351 -0
  123. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +99 -0
  124. data/lib/hecks/fuzzing/properties/querying.rb +206 -0
  125. data/lib/hecks/fuzzing/properties.rb +41 -1065
  126. data/lib/hecks/fuzzing/replay.rb +60 -16
  127. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +6 -2
  128. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +2 -1
  129. data/lib/hecks/fuzzing/sequence_generator.rb +0 -1
  130. data/lib/hecks/fuzzing/value_generator.rb +2 -2
  131. data/lib/hecks/grammar/evolve.rb +34 -10
  132. data/lib/hecks/grammar/expression_operators.json +124 -0
  133. data/lib/hecks/grammar.rb +15 -5
  134. data/lib/hecks/ir.rb +9 -0
  135. data/lib/hecks/language/bluebook/aggregate.bluebook +4 -2
  136. data/lib/hecks/language/bluebook/command.bluebook +2 -1
  137. data/lib/hecks/language/bluebook/entity.bluebook +2 -1
  138. data/lib/hecks/language/bluebook/projection.bluebook +2 -1
  139. data/lib/hecks/language/bluebook/query.bluebook +4 -2
  140. data/lib/hecks/language/bluebook/shape.bluebook +2 -1
  141. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -0
  142. data/lib/hecks/literal.rb +18 -8
  143. data/lib/hecks/naming.rb +6 -3
  144. data/lib/hecks/ports/access_control.rb +1 -1
  145. data/lib/hecks/ports/agent.rb +5 -3
  146. data/lib/hecks/ports/authentication.rb +3 -2
  147. data/lib/hecks/ports/authorization.rb +1 -1
  148. data/lib/hecks/ports/clock.rb +1 -1
  149. data/lib/hecks/ports/extraction.rb +5 -1
  150. data/lib/hecks/ports/identity_assignment.rb +1 -1
  151. data/lib/hecks/ports/identity_generation.rb +1 -1
  152. data/lib/hecks/ports/identity_resolution.rb +1 -1
  153. data/lib/hecks/ports/loading.rb +4 -1
  154. data/lib/hecks/ports/persistence/append_only.rb +51 -4
  155. data/lib/hecks/ports/persistence/binding_policy.rb +1 -1
  156. data/lib/hecks/ports/persistence/plugin.rb +3 -0
  157. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -1
  158. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +5 -5
  159. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +14 -5
  160. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +4 -0
  161. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +1 -1
  162. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +80 -16
  163. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +23 -6
  164. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +43 -5
  165. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +25 -4
  166. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +49 -9
  167. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +2 -2
  168. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +10 -0
  169. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +49 -38
  170. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +77 -18
  171. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +19 -21
  172. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +27 -9
  173. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +9 -0
  174. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +25 -10
  175. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +2 -6
  176. data/lib/hecks/ports/persistence/plugins/era.rb +3 -1
  177. data/lib/hecks/ports/persistence/remote_runtime.rb +1 -1
  178. data/lib/hecks/ports/persistence.rb +7 -3
  179. data/lib/hecks/ports/projection.rb +13 -5
  180. data/lib/hecks/ports/query/in_memory.rb +8 -2
  181. data/lib/hecks/ports/query.rb +1 -3
  182. data/lib/hecks/ports.rb +2 -0
  183. data/lib/hecks/projections/diagrams.rb +11 -3
  184. data/lib/hecks/projections/glossary.rb +374 -0
  185. data/lib/hecks/projections/ir.rb +2 -0
  186. data/lib/hecks/projections/model/deviations.rb +2 -1
  187. data/lib/hecks/projections/model.rb +15 -10
  188. data/lib/hecks/projections/vocabulary.rb +1 -3
  189. data/lib/hecks/projections.rb +1 -0
  190. data/lib/hecks/projector/cli_projector.rb +78 -42
  191. data/lib/hecks/projector/docs_projector.rb +21 -12
  192. data/lib/hecks/projector/exporter.rb +34 -1
  193. data/lib/hecks/projector/narrate_projector.rb +83 -25
  194. data/lib/hecks/projector.rb +2 -2
  195. data/lib/hecks/query_ir.rb +60 -36
  196. data/lib/hecks/query_specification/common/authorization_spec.rb +1 -1
  197. data/lib/hecks/query_specification/common/comparators.rb +4 -0
  198. data/lib/hecks/query_specification/common/comparison.rb +8 -1
  199. data/lib/hecks/query_specification/common/dsl.rb +5 -0
  200. data/lib/hecks/query_specification/common/limit_spec.rb +9 -2
  201. data/lib/hecks/query_specification/common/null_policy.rb +6 -2
  202. data/lib/hecks/query_specification/common/offset_spec.rb +9 -2
  203. data/lib/hecks/query_specification/common/options.rb +17 -5
  204. data/lib/hecks/query_specification/common/order_by.rb +9 -2
  205. data/lib/hecks/query_specification/common/where_clause.rb +15 -2
  206. data/lib/hecks/query_specification/field_path.rb +4 -4
  207. data/lib/hecks/query_specification/read_model/specification.rb +5 -2
  208. data/lib/hecks/router/namespace_installer.rb +22 -10
  209. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +57 -22
  210. data/lib/hecks/runtime/command_interpreter.rb +104 -65
  211. data/lib/hecks/runtime/command_rules/admissibility.rb +26 -8
  212. data/lib/hecks/runtime/command_rules/arithmetic.rb +35 -14
  213. data/lib/hecks/runtime/dependency_planning.rb +33 -21
  214. data/lib/hecks/runtime/dispatcher.rb +41 -7
  215. data/lib/hecks/runtime/entity_element.rb +25 -6
  216. data/lib/hecks/runtime/entity_interpreter.rb +28 -15
  217. data/lib/hecks/runtime/errors.rb +28 -2
  218. data/lib/hecks/runtime/instance.rb +9 -3
  219. data/lib/hecks/runtime/interpreting.rb +45 -9
  220. data/lib/hecks/runtime/loader.rb +21 -0
  221. data/lib/hecks/runtime/outbox.rb +365 -0
  222. data/lib/hecks/runtime/policy_interpreter.rb +32 -17
  223. data/lib/hecks/runtime/query_interpreter.rb +36 -16
  224. data/lib/hecks/runtime/reaction_invocation.rb +35 -22
  225. data/lib/hecks/runtime/read_model_interpreter.rb +31 -7
  226. data/lib/hecks/runtime/reference_hop.rb +1 -1
  227. data/lib/hecks/runtime/refusal_wording.rb +55 -49
  228. data/lib/hecks/runtime/registry/saga_persistence.rb +3 -1
  229. data/lib/hecks/runtime/registry/verification.rb +39 -2
  230. data/lib/hecks/runtime/registry.rb +21 -11
  231. data/lib/hecks/runtime/remote_dispatcher.rb +3 -1
  232. data/lib/hecks/runtime/routing.rb +52 -39
  233. data/lib/hecks/runtime/saga_interpreter/correlation.rb +7 -7
  234. data/lib/hecks/runtime/saga_interpreter.rb +119 -90
  235. data/lib/hecks/runtime/saga_pending_dispatch.rb +6 -3
  236. data/lib/hecks/runtime/tenant_scope.rb +7 -0
  237. data/lib/hecks/runtime/value/coercion.rb +271 -49
  238. data/lib/hecks/runtime/value.rb +1 -1
  239. data/lib/hecks/runtime.rb +7 -2
  240. data/lib/hecks/storehouse.rb +15 -8
  241. data/lib/hecks/version.rb +1 -1
  242. data/lib/hecks/vocabulary.rb +1 -0
  243. data/lib/hecks.rb +17 -13
  244. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +1 -1
  245. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +1 -1
  246. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +1 -1
  247. metadata +16 -2
@@ -0,0 +1,107 @@
1
+ require_relative "evaluator"
2
+ require_relative "resolver"
3
+
4
+ module Hecks
5
+ module Bluebook
6
+ module Expression
7
+ # THE INVERSE OF `AstJson` — reads the `"op"`-tagged JSON a rule row
8
+ # carries as `ast` back into the SAME `Evaluator`/`Resolver` node
9
+ # Structs `Evaluator.parse` builds from `canonical`. This is how the
10
+ # runtime evaluates a rule without re-parsing its text: the one
11
+ # parse happened at DSL-build time, behind `AstJson`; dispatch walks
12
+ # the structured form (`Evaluator.call_rule`), and `canonical` stays
13
+ # what it always displayed as — text for humans and refusal wording.
14
+ #
15
+ # Written as the obvious mirror of `ast_json.rb`'s two walkers, arm
16
+ # for arm, so a new op there is a new arm here and
17
+ # spec/expression_ast_spec.rb's roster contract fails until it
18
+ # lands. Promoted from spec/support once the runtime switched —
19
+ # the spec proved the round trip first, then the runtime adopted it.
20
+ #
21
+ # One deliberate asymmetry, inherited: `AstJson` rewrites a
22
+ # LITERAL-array `.include?` into an OR of equalities (see
23
+ # `emit_include`), so reading never produces an `Include` over an
24
+ # `ArrayLiteral`. Evaluation is unchanged by that rewrite, which is
25
+ # exactly what the equivalence spec pins.
26
+ module AstReader
27
+ module_function
28
+
29
+ def read_predicate(json) = read_bool(json)
30
+
31
+ def read_bool(json)
32
+ case json.fetch("op")
33
+ when "or" then Evaluator::Or.new(left: read_bool(json["left"]), right: read_bool(json["right"]))
34
+ when "and" then Evaluator::And.new(left: read_bool(json["left"]), right: read_bool(json["right"]))
35
+ when "not" then Evaluator::Not.new(node: read_bool(json["expr"]))
36
+ when "compare"
37
+ Evaluator::Compare.new(operator: operator(json["cmp"]),
38
+ left: read_resolver(json["left"]), right: read_resolver(json["right"]))
39
+ when "include"
40
+ Evaluator::Include.new(haystack: read_resolver(json["haystack"]), needle: read_resolver(json["needle"]))
41
+ else Evaluator::Resolve.new(expr: read_resolver(json))
42
+ end
43
+ end
44
+
45
+ # The comparator algebra travels as its three-flag triple; the
46
+ # `Operator` carrying that exact triple is the one `parse` would
47
+ # have chosen, because the roster (`expression/projection.json`)
48
+ # holds one symbol per triple.
49
+ def operator(cmp)
50
+ Evaluator::OPERATORS.find do |op|
51
+ op.compares_less_than == cmp.fetch("less_than") &&
52
+ op.compares_equal == cmp.fetch("equal") &&
53
+ op.negated == cmp.fetch("negated")
54
+ end or raise "no comparison operator has the triple #{cmp.inspect}"
55
+ end
56
+
57
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity -- one arm
58
+ # per AstJson op is the point; splitting the case would hide the roster.
59
+ def read_resolver(json)
60
+ recv = -> { read_resolver(json["receiver"]) }
61
+ case json.fetch("op")
62
+ when "int" then Resolver::IntegerLiteral.new(value: json["value"])
63
+ when "float" then Resolver::FloatLiteral.new(value: json["value"])
64
+ when "str" then Resolver::StringLiteral.new(value: json["value"])
65
+ when "bool" then Resolver::BoolLiteral.new(value: json["value"])
66
+ when "nil" then Resolver::NilLiteral.new
67
+ when "array" then Resolver::ArrayLiteral.new(elements: json["elements"].map { |e| read_resolver(e) })
68
+ when "lookup" then Resolver::Lookup.new(path: json["path"].join("."))
69
+ when "add" then Resolver::Addition.new(left: read_resolver(json["left"]), right: read_resolver(json["right"]))
70
+ when "sign_test"
71
+ op = operator(json["cmp"])
72
+ Resolver::SignTest.new(operator: op, test: sign_test_name(op), receiver: recv.call)
73
+ when "empty" then Resolver::Empty.new(receiver: recv.call)
74
+ when "to_s" then Resolver::ToS.new(receiver: recv.call)
75
+ when "modulo" then Resolver::Modulo.new(receiver: recv.call, divisor: read_resolver(json["divisor"]))
76
+ when "size" then Resolver::Size.new(receiver: recv.call)
77
+ when "first" then Resolver::First.new(receiver: recv.call)
78
+ when "last" then Resolver::Last.new(receiver: recv.call)
79
+ when "block_predicate"
80
+ Resolver::BlockPredicate.new(mode: json["mode"].to_sym, receiver: recv.call, param: json["param"],
81
+ predicate: read_bool(json["predicate"]))
82
+ when "find"
83
+ Resolver::Find.new(receiver: recv.call, param: json["param"],
84
+ predicate: read_bool(json["predicate"]), path: json["path"])
85
+ when "matches_regex" then Resolver::MatchesRegex.new(receiver: recv.call, pattern: json["pattern"],
86
+ flags: json["flags"])
87
+ when "presence" then Resolver::Presence.new(receiver: recv.call, negated: json["negated"])
88
+ when "assignment" then Resolver::Assignment.new(receiver: recv.call, negated: json["negated"])
89
+ when "split" then Resolver::Split.new(receiver: recv.call, separator: json["separator"])
90
+ when "starts_with" then Resolver::StartsWith.new(receiver: recv.call, substring: json["substring"])
91
+ when "ends_with" then Resolver::EndsWith.new(receiver: recv.call, substring: json["substring"])
92
+ else raise "no reader handles op #{json['op'].inspect} — add an arm before AstJson can emit it"
93
+ end
94
+ end
95
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
96
+
97
+ # `SignTest#test` is only wording (the refusal names it); the
98
+ # triple is what evaluates. Recover the spelling from the
99
+ # vocabulary so a rebuilt node refuses with the same message the
100
+ # parsed one would.
101
+ def sign_test_name(operator)
102
+ Resolver::SIGN_TEST_OPERATORS.key(operator.symbol) || operator.symbol
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -4,6 +4,13 @@ require_relative "../../vocabulary"
4
4
  module Hecks
5
5
  module Bluebook
6
6
  module Expression
7
+ # Rewrites a predicate's SOURCE TEXT into one canonical spelling
8
+ # before it's ever parsed or hashed — collapsing whitespace and
9
+ # folding admitted synonyms (e.g. `.length` → `.size`) via the
10
+ # `RULES` table projected from the grammar chapter, so two byte-
11
+ # different but equivalent predicates compare and cache identically.
12
+ # Never touches text inside quoted string literals
13
+ # (`map_outside_strings`).
7
14
  module CanonicalForm
8
15
  Rule = Struct.new(:strategy, :source_token, :replacement, :boundary, :position, keyword_init: true)
9
16
 
@@ -4,6 +4,13 @@ require_relative "../../vocabulary"
4
4
  module Hecks
5
5
  module Bluebook
6
6
  module Expression
7
+ # The boolean/comparison layer of a predicate's expression grammar —
8
+ # parses a canonical predicate string into an Or/And/Not/Compare/
9
+ # Include/Resolve AST (cached per distinct string, `ast_cache`) and
10
+ # interprets it against a call's own state/attrs. Leaf `Resolve`
11
+ # nodes delegate to `Resolver` for the dotted/arithmetic
12
+ # sub-grammar; `OPERATORS` is the comparison table projected from
13
+ # the grammar chapter (`PROJECTION`).
7
14
  module Evaluator
8
15
  Operator = Struct.new(:symbol, :compares_less_than, :compares_equal, :negated, keyword_init: true)
9
16
 
@@ -58,6 +65,54 @@ module Hecks
58
65
  interpret(ast_cache[expr] ||= parse(expr), state, attrs)
59
66
  end
60
67
 
68
+ # THE RULE-SHAPED ENTRY — evaluates a Given/Invariant (anything
69
+ # answering `canonical` and `ast`) by walking its STRUCTURED form,
70
+ # never re-parsing the text: the one parse happened at DSL-build
71
+ # time behind `AstJson`, and `AstReader` turns that tree back into
72
+ # the same nodes `parse` would have built (the equivalence is
73
+ # pinned by spec/expression_ast_spec.rb over the bounded-
74
+ # exhaustive generator). A rule with no `ast` (an Assembly-
75
+ # rebuilt one, or a placeholder resolved outside build_rule)
76
+ # falls back to parsing its canonical — same cache, same key.
77
+ #
78
+ # HECKS_EVAL=string reverts to the text path wholesale, kept for
79
+ # one release as the escape hatch while the ast path beds in.
80
+ def call_rule(rule, state, attrs = {})
81
+ return call(rule.canonical, state, attrs) if ENV["HECKS_EVAL"] == "string"
82
+
83
+ interpret(ast_cache[rule.canonical] ||= nodes_for(rule), state, attrs)
84
+ end
85
+
86
+ def nodes_for(rule)
87
+ rule.ast ? AstReader.read_predicate(rule.ast) : parse(rule.canonical)
88
+ end
89
+
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
103
+ # missing diagnostic is a worse debugging experience than none, a
104
+ # crash while building one is worse still.
105
+ def comparison_detail(expr, state, attrs = {})
106
+ node = ast_cache[expr] ||= parse(expr)
107
+ return nil unless node.is_a?(Compare)
108
+
109
+ lhs = Resolver.interpret(node.left, state, attrs)
110
+ rhs = Resolver.interpret(node.right, state, attrs)
111
+ "left: #{Resolver.describe(lhs)}, right: #{Resolver.describe(rhs)}"
112
+ rescue EvaluationError
113
+ nil
114
+ end
115
+
61
116
  def parse(expr)
62
117
  expr = strip_parens(expr.to_s.strip)
63
118
 
@@ -117,21 +172,21 @@ module Hecks
117
172
  end
118
173
  end
119
174
 
120
- def compare(op, left, right, state, attrs)
175
+ def compare(comparator, left, right, state, attrs)
121
176
  lhs = Resolver.interpret(left, state, attrs)
122
177
  rhs = Resolver.interpret(right, state, attrs)
123
178
 
124
- apply(op, lhs, rhs)
179
+ apply(comparator, lhs, rhs)
125
180
  end
126
181
 
127
182
  # The algebra itself, on values already resolved — split out so a sign
128
183
  # test (SignTest#compares_via names an Operator symbol) can apply the
129
184
  # SAME primitives compare() uses against the literal 0, rather than
130
185
  # re-deriving positive?/negative?/zero? by hand a second time.
131
- def apply(op, lhs, rhs)
132
- result = (op.compares_less_than && less_than(lhs, rhs)) ||
133
- (op.compares_equal && equal?(lhs, rhs))
134
- op.negated ? !result : result
186
+ def apply(comparator, lhs, rhs)
187
+ result = (comparator.compares_less_than && less_than(lhs, rhs)) ||
188
+ (comparator.compares_equal && equal?(lhs, rhs))
189
+ comparator.negated ? !result : result
135
190
  end
136
191
 
137
192
  def less_than(lhs, rhs)
@@ -198,9 +253,7 @@ module Hecks
198
253
  case (found = Resolver.interpret(haystack, state, attrs))
199
254
  when Array then found.any? { |item| equal?(item, wanted) }
200
255
  when String
201
- unless wanted.is_a?(String)
202
- raise EvaluationError, "no implicit conversion of #{class_of(wanted)} into String"
203
- end
256
+ raise EvaluationError, "no implicit conversion of #{class_of(wanted)} into String" unless wanted.is_a?(String)
204
257
 
205
258
  found.include?(wanted)
206
259
  else false
@@ -286,9 +339,9 @@ module Hecks
286
339
  # its own (`[0, 0 + 0]`) used to read as a split point for
287
340
  # THIS expression's own boolean/comparison grammar, exactly
288
341
  # the way an un-tracked `{`/`}` once did for block predicates.
289
- elsif char == "(" || char == "{" || char == "["
342
+ elsif ["(", "{", "["].include?(char)
290
343
  depth += 1
291
- elsif char == ")" || char == "}" || char == "]"
344
+ elsif [")", "}", "]"].include?(char)
292
345
  depth -= 1
293
346
  elsif depth.zero? && expr[index, operator.length] == operator
294
347
  return index if !block_given? || yield(index)
@@ -197,6 +197,18 @@
197
197
  "category": "positional",
198
198
  "precedence": 7,
199
199
  "arity": 1
200
+ },
201
+ {
202
+ "symbol": ".set?",
203
+ "category": "presence",
204
+ "precedence": 7,
205
+ "arity": 1
206
+ },
207
+ {
208
+ "symbol": ".unset?",
209
+ "category": "presence",
210
+ "precedence": 7,
211
+ "arity": 1
200
212
  }
201
213
  ],
202
214
  "normalisations": [
@@ -12,6 +12,9 @@
12
12
  module Hecks
13
13
  module Bluebook
14
14
  module Expression
15
+ # Reopens the SAME `Resolver` module resolver.rb defines — see this
16
+ # file's own header comment above for why the block-predicate/find
17
+ # family lives here rather than in resolver.rb itself.
15
18
  module Resolver
16
19
  # `receiver.all? { |x| PREDICATE }` / `.any? { ... }` / `.none? {
17
20
  # ... }` -- vendored addition, not (yet) upstream hecks
@@ -184,9 +187,7 @@ module Hecks
184
187
  # just `attrs` extended with the bound name for the span of that
185
188
  # one predicate evaluation, discarded immediately after.
186
189
  def evaluate_block_predicate(node, collection, state, attrs)
187
- unless collection.is_a?(Array)
188
- raise EvaluationError, "#{node.mode}? expects a list, got #{describe(collection)}"
189
- end
190
+ raise EvaluationError, "#{node.mode}? expects a list, got #{describe(collection)}" unless collection.is_a?(Array)
190
191
 
191
192
  outcomes = collection.map { |element| interpret_with_element(node, element, state, attrs) }
192
193
 
@@ -8,6 +8,14 @@ module Hecks
8
8
  module Expression
9
9
  class EvaluationError < StandardError; end
10
10
 
11
+ # The dotted/arithmetic leaf grammar a predicate's `Resolve` node
12
+ # bottoms out in — attribute lookups, literals, arithmetic, and the
13
+ # growing set of vendored method-call suffixes (`.match?`,
14
+ # `.present?`/`.blank?`, `.set?`/`.unset?`, `.split`, `.first`/
15
+ # `.last`, `.all?`/`.any?`/`.none?`/`.find`, ...) this file and its
16
+ # `resolver/block_predicates.rb` sibling parse and interpret. Each
17
+ # leaf's `parse` is a pure function of its own string, cached at the
18
+ # `Evaluator` layer; `interpret` reads state/attrs fresh each call.
11
19
  module Resolver
12
20
  SIGN_TESTS = Hecks::Vocabulary.fetch("SignTest")
13
21
 
@@ -109,6 +117,24 @@ module Hecks
109
117
  # field that could legitimately be "" rather than absent.
110
118
  Presence = Struct.new(:receiver, :negated, keyword_init: true)
111
119
 
120
+ # `.set?`/`.unset?` -- sibling of `.present?`/`.blank?` immediately
121
+ # above, added for a DELIBERATELY narrower question, spelled out in
122
+ # the name so choosing between the two pairs is a choice, not a
123
+ # trap: `.present?` means "not EMPTY" (Rails-standard -- nil/false,
124
+ # and an EMPTY String/Array/Hash, are blank; nothing else is),
125
+ # 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
128
+ # conflation is a real trap -- confirmed live: a corpus author
129
+ # reaching for `superseded_by.blank?` to guard an optional
130
+ # reference burned real time before landing on "just don't guard
131
+ # it," because `.blank?` cannot be asked to mean ONLY "was this
132
+ # never assigned." `.set?`/`.unset?` ask exactly that, and nothing
133
+ # else: `!receiver.nil?`, full stop -- an assigned-but-empty
134
+ # String/Array/Hash (or a VO wrapping one) is `.set?`, not
135
+ # `.unset?`, unlike `.blank?`'s own reading of the identical value.
136
+ Assignment = Struct.new(:receiver, :negated, keyword_init: true)
137
+
112
138
  # `receiver.split("SEP")` -- vendored addition, not (yet) upstream
113
139
  # hecks (migration plan task 9): confirmed the single
114
140
  # highest-value new finding of the real-dispatch smoke sweep --
@@ -186,6 +212,18 @@ module Hecks
186
212
  interpret(parse(expr), state, attrs)
187
213
  end
188
214
 
215
+ # A grammar's own dispatch table — one `return Node.new(...) if
216
+ # expr =~ /pattern/` per leaf production, in a FIXED precedence
217
+ # order (`.length` before the generic suffixes, sign tests before
218
+ # the rest, etc. — see this file's own history comments on why
219
+ # several of these were added in exactly this position). Each
220
+ # branch is already one line; splitting the table into smaller
221
+ # methods would not shrink any single check, only hide the
222
+ # precedence order this method's own line-by-line sequence IS.
223
+ # rubocop:disable-next Metrics/AbcSize
224
+ # rubocop:disable-next Metrics/CyclomaticComplexity
225
+ # rubocop:disable-next Metrics/MethodLength
226
+ # rubocop:disable-next Metrics/PerceivedComplexity
189
227
  def parse(expr)
190
228
  expr = expr.to_s.strip
191
229
 
@@ -215,7 +253,7 @@ module Hecks
215
253
 
216
254
  return Size.new(receiver: parse(Regexp.last_match(1))) if expr =~ /\A(.+)\.size\z/
217
255
 
218
- if expr =~ /\A(.+)\.match\?\(\/(.*)\/([a-z]*)\)\z/m
256
+ if expr =~ %r{\A(.+)\.match\?\(/(.*)/([a-z]*)\)\z}m
219
257
  return MatchesRegex.new(receiver: parse(Regexp.last_match(1)),
220
258
  pattern: Regexp.last_match(2),
221
259
  flags: Regexp.last_match(3))
@@ -224,19 +262,25 @@ module Hecks
224
262
  return Presence.new(receiver: parse(Regexp.last_match(1)), negated: false) if expr =~ /\A(.+)\.present\?\z/
225
263
  return Presence.new(receiver: parse(Regexp.last_match(1)), negated: true) if expr =~ /\A(.+)\.blank\?\z/
226
264
 
265
+ return Assignment.new(receiver: parse(Regexp.last_match(1)), negated: false) if expr =~ /\A(.+)\.set\?\z/
266
+ return Assignment.new(receiver: parse(Regexp.last_match(1)), negated: true) if expr =~ /\A(.+)\.unset\?\z/
267
+
227
268
  if expr =~ /\A(.+)\.split\("([^"]*)"\)\z/
228
- return Split.new(receiver: parse(Regexp.last_match(1)), separator: Regexp.last_match(2))
269
+ return Split.new(receiver: parse(Regexp.last_match(1)),
270
+ separator: Regexp.last_match(2))
229
271
  end
230
272
 
231
273
  return First.new(receiver: parse(Regexp.last_match(1))) if expr =~ /\A(.+)\.first\z/
232
274
  return Last.new(receiver: parse(Regexp.last_match(1))) if expr =~ /\A(.+)\.last\z/
233
275
 
234
276
  if expr =~ /\A(.+)\.start_with\?\("([^"]*)"\)\z/
235
- return StartsWith.new(receiver: parse(Regexp.last_match(1)), substring: Regexp.last_match(2))
277
+ return StartsWith.new(receiver: parse(Regexp.last_match(1)),
278
+ substring: Regexp.last_match(2))
236
279
  end
237
280
 
238
281
  if expr =~ /\A(.+)\.end_with\?\("([^"]*)"\)\z/
239
- return EndsWith.new(receiver: parse(Regexp.last_match(1)), substring: Regexp.last_match(2))
282
+ return EndsWith.new(receiver: parse(Regexp.last_match(1)),
283
+ substring: Regexp.last_match(2))
240
284
  end
241
285
 
242
286
  block_opener = parse_block_opener(expr)
@@ -252,6 +296,16 @@ module Hecks
252
296
  SignTest.new(operator: operator, test: test, receiver: parse(receiver))
253
297
  end
254
298
 
299
+ # `parse`'s own dispatch table, mirrored: one `when` per leaf node
300
+ # type it can produce, each already delegating to a small named
301
+ # helper (add, apply_sign_test, emptiness_of, ...) — the case
302
+ # itself IS the closed, declared set this method exists to
303
+ # exhaust; the `else` backstop's own comment explains why a
304
+ # missing arm is a bug this method is built to make loud, not
305
+ # quiet.
306
+ # rubocop:disable-next Metrics/AbcSize
307
+ # rubocop:disable-next Metrics/CyclomaticComplexity
308
+ # rubocop:disable-next Metrics/MethodLength
255
309
  def interpret(node, state, attrs)
256
310
  case node
257
311
  when IntegerLiteral, FloatLiteral, StringLiteral, BoolLiteral then node.value
@@ -274,6 +328,9 @@ module Hecks
274
328
  when Presence
275
329
  present = !blank?(interpret(node.receiver, state, attrs))
276
330
  node.negated ? !present : present
331
+ when Assignment
332
+ set = !interpret(node.receiver, state, attrs).nil?
333
+ node.negated ? !set : set
277
334
  when Split
278
335
  split_value(interpret(node.receiver, state, attrs), node.separator)
279
336
  when Last
@@ -339,8 +396,7 @@ module Hecks
339
396
  # found live while building this, not assumed).
340
397
  def matches_regex?(receiver_value, pattern, flags)
341
398
  text = case receiver_value
342
- when String, Symbol then receiver_value.to_s
343
- when Integer, Float then receiver_value.to_s
399
+ when String, Symbol, Integer, Float then receiver_value.to_s
344
400
  when NilClass then ""
345
401
  else
346
402
  raise EvaluationError, "match? expects a scalar, got #{receiver_value.class}"
@@ -442,8 +498,25 @@ module Hecks
442
498
  nil
443
499
  end
444
500
 
501
+ # Integer is signed 64-bit (C3.3, docs/semantics/bluebook-semantics.md)
502
+ # — Ruby's own Integer would promote past ±2^63-1 silently; the
503
+ # language does not, so a sum outside that range is an evaluation
504
+ # fault, worded exactly as the Rust kernel's `checked_add` words
505
+ # it. A Float sum that is not finite is the same fault (C3.4).
506
+ INT64_RANGE = (-(2**63))..((2**63) - 1)
507
+
445
508
  def add(left, right)
446
- require_number(left, "addition") + require_number(right, "addition")
509
+ lhs = require_number(left, "addition")
510
+ rhs = require_number(right, "addition")
511
+ sum = lhs + rhs
512
+ if sum.is_a?(Integer)
513
+ return sum if INT64_RANGE.cover?(sum)
514
+
515
+ raise EvaluationError, "addition overflowed: #{lhs} + #{rhs} does not fit in a 64-bit integer"
516
+ end
517
+ return sum if sum.finite?
518
+
519
+ raise EvaluationError, "addition overflowed: #{lhs} + #{rhs} is not a finite number"
447
520
  end
448
521
 
449
522
  def quoted?(expr)
@@ -533,10 +606,9 @@ module Hecks
533
606
 
534
607
  def string_of(value)
535
608
  case value
536
- when String then value
537
- when Integer, Float then value.to_s
538
- when TrueClass, FalseClass then value.to_s
539
- when NilClass then ""
609
+ when String then value
610
+ when Integer, Float, TrueClass, FalseClass then value.to_s
611
+ when NilClass then ""
540
612
  else
541
613
  raise EvaluationError, "to_s expects a scalar, got #{describe(value)}"
542
614
  end
@@ -11,4 +11,5 @@ end
11
11
  require_relative "expression/canonical_form"
12
12
  require_relative "expression/resolver"
13
13
  require_relative "expression/evaluator"
14
+ require_relative "expression/ast_reader"
14
15
  require_relative "expression/ast_json"
@@ -18,6 +18,11 @@ module Hecks
18
18
  def aggregate_name = Naming.demodulise(aggregate)
19
19
  end
20
20
 
21
+ # The built form of a `.hecksagon` file, produced by
22
+ # `DSL::HecksagonBuilder` — a domain's own binds (`Bind`, above),
23
+ # subscriptions, and attached framework/vendored members.
24
+ # `Behaviour::Hecksagon` supplies the bind lookups (`bind_for`/
25
+ # `binds_for`); this class holds only the declared data.
21
26
  class Hecksagon
22
27
  include Hecks::IR
23
28
  include Behaviour::Hecksagon
@@ -41,6 +46,11 @@ module Hecks
41
46
  end
42
47
  end
43
48
 
49
+ # The built form of a `.world` file, produced by `DSL::WorldBuilder` —
50
+ # a domain's own `realm`/`latest` version markers and its adapter bind
51
+ # SETTINGS (as opposed to `Hecksagon`'s own bind LIST, above).
52
+ # `Behaviour::World` supplies the settings lookups (`for_verb`/
53
+ # `for_binding`); this class holds only the declared data.
44
54
  class World
45
55
  include Hecks::IR
46
56
  include Behaviour::World
@@ -2,6 +2,10 @@ require_relative "behaviour/lifecycle"
2
2
 
3
3
  module Hecks
4
4
  module Bluebook
5
+ # One declared `transition` row — the state a command moves an
6
+ # aggregate or entity TO, and, when guarded, the state(s) it must
7
+ # currently be in (`from:`) for the transition to apply. An unguarded
8
+ # transition (`from: nil`) applies from any current state.
5
9
  class StateTransition
6
10
  attr_reader :target, :from
7
11
 
@@ -17,6 +21,11 @@ module Hecks
17
21
  def constrained? = !@from.nil?
18
22
  end
19
23
 
24
+ # The built form of a `lifecycle :field, default: ... do ... end`
25
+ # block, produced by `DSL::LifecycleBuilder` — the field a state
26
+ # machine lives on, its starting value, and the declared transition
27
+ # table. `Behaviour::Lifecycle` supplies the reads (`states`,
28
+ # `target_for`, ...) built purely from this data.
20
29
  class Lifecycle
21
30
  include Hecks::IR
22
31
  include Behaviour::Lifecycle
@@ -99,7 +99,7 @@ module Hecks
99
99
  value
100
100
  end
101
101
 
102
- def args(pairs) = pairs.reject { |_, value| value.nil? }
102
+ def args(pairs) = pairs.compact
103
103
 
104
104
  def offer(label)
105
105
  yield
@@ -352,7 +352,10 @@ module Hecks
352
352
  # meta store happened to iterate in insertion order — an accident that an
353
353
  # ask ordered any other way would have taken away, and Reconstruction is
354
354
  # the one reader that must have the SOURCE'S order rather than a stable one.
355
- POSITION = "position"
355
+ # `private` above has no effect on a constant; kept here anyway,
356
+ # beside the method that reads it, for the narrative.
357
+ # rubocop:disable-next Lint/UselessConstantScoping
358
+ POSITION = "position".freeze
356
359
 
357
360
  def declare(plan, category, node, id, parent_id, index, extra = {})
358
361
  return unless plan.declare
@@ -425,7 +428,7 @@ module Hecks
425
428
  category == "Entity" ? parent_id : id
426
429
  end
427
430
 
428
- def sealers(plan, category, receiver)
431
+ def sealers(plan, _category, receiver)
429
432
  id = receiver[:entities].last || receiver[:aggregate]
430
433
  plan.sealers.each { |verb| send_to("Bluebook::#{verb_for(plan, verb)}", id, to: receiver) }
431
434
  end
@@ -586,7 +589,10 @@ module Hecks
586
589
  # `field_value` ; it is read the same way `plan.parent_key` already is,
587
590
  # because it IS that fact, spelled for the walk's immediate parent
588
591
  # rather than for one specific kind of one.
589
- OWNER = "owner_id"
592
+ # `private` above has no effect on a constant; kept here anyway,
593
+ # beside the method that reads it, for the narrative.
594
+ # rubocop:disable-next Lint/UselessConstantScoping
595
+ OWNER = "owner_id".freeze
590
596
 
591
597
  def identity_part(plan, path, parent_id, node, index, category)
592
598
  head = path.to_s.split(".").first
@@ -35,6 +35,10 @@ module Hecks
35
35
  # nothing — that is the judge's job.
36
36
  class Plan
37
37
  # One append command: the verb, and the value-object-field -> argument map.
38
+ # `:map` shadows Enumerable#map on purpose — `spec/plan_spec.rb`
39
+ # itself asserts `appends["emits"].map` reads the field (a Hash),
40
+ # not an Enumerable#map result. Verified before disabling this cop.
41
+ # rubocop:disable-next Lint/StructNewOverride
38
42
  Append = Struct.new(:verb, :map, keyword_init: true)
39
43
 
40
44
  # One setting command. `targets` is target -> argument ; Lifecycle sets two
@@ -148,7 +148,7 @@ module Hecks
148
148
  # against, so the entity ones have to be told apart by `entity_id`. Rejecting
149
149
  # from an ORDERED read keeps the order.
150
150
  def own(category, aggregate_id)
151
- declared(category, aggregate_id).reject { |row| text(row[:entity_id]).to_s != "" }
151
+ declared(category, aggregate_id).select { |row| text(row[:entity_id]).to_s == "" }
152
152
  end
153
153
 
154
154
  # A piece's own verbs and asks. There is no `DeclaredIn` keyed by entity, so
@@ -238,12 +238,12 @@ module Hecks
238
238
  # Mutation's own `to_h` lambda does, rather than leaving the key
239
239
  # silently absent (spec/round_trip_spec's whole point: a field the
240
240
  # language does not hold is a named gap, not a byte-for-byte one).
241
- def mutation(target, op, bindings)
242
- base = { target: target.to_sym, op: op.to_sym, sign: Hecks::Bluebook::Mutation.sign_for(op) }
241
+ def mutation(target, oper, bindings)
242
+ base = { target: target.to_sym, op: oper.to_sym, sign: Hecks::Bluebook::Mutation.sign_for(oper) }
243
243
  # `:delegate`/`:corrects` (CommandBuilder#delegates_to's and
244
244
  # #corrects_impl's own comments) ride the SAME multi-binding
245
245
  # shape `:append` does.
246
- return base.merge(fields: appended(bindings)) if ["append", "delegate", "corrects"].include?(op)
246
+ return base.merge(fields: appended(bindings)) if ["append", "delegate", "corrects"].include?(oper)
247
247
 
248
248
  base.merge(source: classified(bindings.first))
249
249
  end
@@ -85,7 +85,9 @@ module Hecks
85
85
 
86
86
  def same_chapters?(cached, current)
87
87
  cached.size == current.size &&
88
- cached.zip(current).all? { |(cached_name, cached_chapter), (name, chapter)| cached_name == name && cached_chapter.equal?(chapter) }
88
+ cached.zip(current).all? do |(cached_name, cached_chapter), (name, chapter)|
89
+ cached_name == name && cached_chapter.equal?(chapter)
90
+ end
89
91
  end
90
92
 
91
93
  def boot
@@ -140,7 +142,8 @@ module Hecks
140
142
  def declare_syntax(runtime, bluebook)
141
143
  syntax = bluebook.aggregate("Syntax")
142
144
  runtime.dispatch("Bluebook::Bluebook.Declare", name: v(bluebook.hecks_name),
143
- vision: v("the language's own grammar table, dispatched into itself"),
145
+ vision: v("the language's own grammar table, " \
146
+ "dispatched into itself"),
144
147
  classification: v("core"))
145
148
  runtime.dispatch("Bluebook::Syntax.Declare", bluebook: bluebook.hecks_name, name: v(syntax.hecks_name))
146
149
  end
@@ -239,8 +242,8 @@ module Hecks
239
242
  end
240
243
 
241
244
  def stringify(row)
242
- row.to_h.each_with_object({}) do |(key, cell), out|
243
- out[key] = scalar(cell).to_s
245
+ row.to_h.transform_values do |cell|
246
+ scalar(cell).to_s
244
247
  end
245
248
  end
246
249