hecks 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (373) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +72 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +83 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
  7. data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
  9. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  10. data/lib/hecks/adapters/driven/heki.rb +106 -9
  11. data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
  12. data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
  13. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  15. data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
  16. data/lib/hecks/adapters/driven/lambda.rb +82 -35
  17. data/lib/hecks/adapters/driven/local_storage.rb +83 -10
  18. data/lib/hecks/adapters/driven/memory.rb +205 -9
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -1
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  24. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  25. data/lib/hecks/adapters/driven/prism.rb +50 -11
  26. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  31. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  32. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  33. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  34. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  35. data/lib/hecks/adapters/driven.rb +6 -4
  36. data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
  37. data/lib/hecks/behaviors/dsl.rb +60 -2
  38. data/lib/hecks/behaviors/expectations.rb +190 -29
  39. data/lib/hecks/behaviors/ir.rb +12 -1
  40. data/lib/hecks/behaviors/rspec.rb +9 -1
  41. data/lib/hecks/behaviors/runner.rb +21 -2
  42. data/lib/hecks/behaviors.rb +9 -1
  43. data/lib/hecks/bluebook/aggregate.rb +43 -13
  44. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
  45. data/lib/hecks/bluebook/assembly/build.rb +26 -5
  46. data/lib/hecks/bluebook/assembly/contract.rb +98 -23
  47. data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
  48. data/lib/hecks/bluebook/assembly/marks.rb +159 -30
  49. data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
  50. data/lib/hecks/bluebook/assembly.rb +32 -14
  51. data/lib/hecks/bluebook/attribute.rb +26 -12
  52. data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
  53. data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
  54. data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
  55. data/lib/hecks/bluebook/behaviour/command.rb +55 -25
  56. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
  57. data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
  58. data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
  59. data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
  60. data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
  61. data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
  62. data/lib/hecks/bluebook/behaviour/query.rb +6 -1
  63. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
  64. data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
  65. data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
  66. data/lib/hecks/bluebook/capabilities.rb +27 -0
  67. data/lib/hecks/bluebook/chapter.rb +51 -9
  68. data/lib/hecks/bluebook/command.rb +62 -17
  69. data/lib/hecks/bluebook/domain_port.rb +34 -9
  70. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  71. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  72. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +282 -123
  73. data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
  74. data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
  75. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
  76. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
  77. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  78. data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
  79. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  80. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  81. data/lib/hecks/bluebook/dsl/entity_builder.rb +191 -61
  82. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  83. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
  84. data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
  85. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  86. data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
  87. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  88. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  89. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +111 -47
  90. data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
  91. data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
  92. data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
  93. data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
  94. data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
  95. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  96. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  97. data/lib/hecks/bluebook/entity.rb +40 -11
  98. data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
  99. data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
  100. data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
  101. data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
  102. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
  103. data/lib/hecks/bluebook/expression/resolver.rb +369 -128
  104. data/lib/hecks/bluebook/hexagon.rb +35 -1
  105. data/lib/hecks/bluebook/lifecycle.rb +12 -1
  106. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
  107. data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
  108. data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
  109. data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
  110. data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
  111. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
  112. data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
  113. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
  114. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
  115. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
  116. data/lib/hecks/bluebook/meta_validator.rb +235 -139
  117. data/lib/hecks/bluebook/model_check.rb +434 -104
  118. data/lib/hecks/bluebook/pattern_subset.rb +32 -10
  119. data/lib/hecks/bluebook/policy.rb +15 -13
  120. data/lib/hecks/bluebook/process_manager.rb +27 -14
  121. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  122. data/lib/hecks/bluebook/project_loader.rb +40 -0
  123. data/lib/hecks/bluebook/project_register.rb +50 -6
  124. data/lib/hecks/bluebook/query.rb +31 -4
  125. data/lib/hecks/bluebook/read_model.rb +35 -15
  126. data/lib/hecks/bluebook/reference.rb +26 -13
  127. data/lib/hecks/bluebook/smoke_test.rb +46 -23
  128. data/lib/hecks/bluebook/synthesizer.rb +46 -12
  129. data/lib/hecks/bluebook/translation.rb +34 -5
  130. data/lib/hecks/bluebook/value_object.rb +29 -11
  131. data/lib/hecks/bluebook.rb +5 -6
  132. data/lib/hecks/codemod.rb +138 -50
  133. data/lib/hecks/construct.rb +21 -7
  134. data/lib/hecks/corpus.rb +438 -0
  135. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  136. data/lib/hecks/doc/reference.rb +200 -31
  137. data/lib/hecks/embryonaut_bluebook.rb +38 -15
  138. data/lib/hecks/facade/cli_door.rb +69 -10
  139. data/lib/hecks/facade/cli_runner.rb +105 -24
  140. data/lib/hecks/facade/command_request.rb +23 -0
  141. data/lib/hecks/facade/handle.rb +155 -35
  142. data/lib/hecks/facade/json_door.rb +106 -25
  143. data/lib/hecks/facade/surface/aggregate_door.rb +50 -27
  144. data/lib/hecks/facade/surface/chapter.rb +26 -17
  145. data/lib/hecks/facade/surface.rb +16 -3
  146. data/lib/hecks/facade.rb +15 -4
  147. data/lib/hecks/forms/app.rb +46 -30
  148. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  149. data/lib/hecks/forms/field_renderer.rb +142 -6
  150. data/lib/hecks/forms/field_shape.rb +183 -19
  151. data/lib/hecks/forms/html.rb +51 -7
  152. data/lib/hecks/forms/index_renderer.rb +14 -2
  153. data/lib/hecks/forms/page.rb +14 -0
  154. data/lib/hecks/forms/params.rb +120 -23
  155. data/lib/hecks/forms/port_argument.rb +14 -2
  156. data/lib/hecks/forms/query_form_renderer.rb +65 -2
  157. data/lib/hecks/forms/record_renderer.rb +60 -2
  158. data/lib/hecks/forms/record_table.rb +28 -1
  159. data/lib/hecks/forms/reference_options.rb +24 -0
  160. data/lib/hecks/forms/value_object_shape.rb +13 -3
  161. data/lib/hecks/forms.rb +24 -4
  162. data/lib/hecks/fqn.rb +59 -1
  163. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  164. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  165. data/lib/hecks/framework/oidc.json +15 -0
  166. data/lib/hecks/framework.rb +79 -25
  167. data/lib/hecks/freezer.rb +27 -11
  168. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
  169. data/lib/hecks/fuzzing/combination_miner.rb +178 -0
  170. data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
  171. data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
  172. data/lib/hecks/fuzzing/differential.rb +192 -0
  173. data/lib/hecks/fuzzing/domain_generator.rb +871 -0
  174. data/lib/hecks/fuzzing/era_boundary.rb +68 -19
  175. data/lib/hecks/fuzzing/form_census.rb +121 -20
  176. data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
  177. data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
  178. data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
  179. data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
  180. data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
  181. data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
  182. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
  183. data/lib/hecks/fuzzing/properties/guards.rb +86 -42
  184. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
  185. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
  186. data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
  187. data/lib/hecks/fuzzing/properties/querying.rb +82 -28
  188. data/lib/hecks/fuzzing/properties.rb +84 -51
  189. data/lib/hecks/fuzzing/qa_settings.rb +164 -0
  190. data/lib/hecks/fuzzing/replay.rb +241 -133
  191. data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
  192. data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
  193. data/lib/hecks/fuzzing/self_consistency.rb +315 -137
  194. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
  195. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
  196. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
  197. data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
  198. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
  199. data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
  200. data/lib/hecks/fuzzing/shrinker.rb +309 -0
  201. data/lib/hecks/fuzzing/structural_skips.rb +37 -130
  202. data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
  203. data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
  204. data/lib/hecks/fuzzing/value_generator.rb +110 -22
  205. data/lib/hecks/fuzzing.rb +5 -0
  206. data/lib/hecks/grammar/evolve.rb +188 -12
  207. data/lib/hecks/grammar.rb +53 -7
  208. data/lib/hecks/ir.rb +51 -20
  209. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  210. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  211. data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
  212. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  213. data/lib/hecks/language/oidc.json +5 -0
  214. data/lib/hecks/literal.rb +41 -9
  215. data/lib/hecks/naming.rb +112 -31
  216. data/lib/hecks/ports/access_control.rb +53 -2
  217. data/lib/hecks/ports/agent/answers.rb +83 -6
  218. data/lib/hecks/ports/agent.rb +119 -35
  219. data/lib/hecks/ports/authentication.rb +44 -4
  220. data/lib/hecks/ports/authorization.rb +50 -11
  221. data/lib/hecks/ports/clock.rb +42 -23
  222. data/lib/hecks/ports/extraction.rb +16 -0
  223. data/lib/hecks/ports/identity_assignment.rb +23 -2
  224. data/lib/hecks/ports/identity_generation.rb +17 -3
  225. data/lib/hecks/ports/identity_resolution.rb +17 -1
  226. data/lib/hecks/ports/key_vault.port +6 -0
  227. data/lib/hecks/ports/key_vault.rb +58 -0
  228. data/lib/hecks/ports/loading.rb +4 -0
  229. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  230. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  231. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  232. data/lib/hecks/ports/persistence/execution.rb +4 -0
  233. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  234. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  235. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  236. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  237. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  238. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  239. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +34 -5
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  250. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
  251. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  252. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +95 -6
  253. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  254. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
  255. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  256. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  257. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  258. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +48 -8
  259. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  260. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  261. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  262. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +58 -19
  263. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
  264. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  265. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -1
  266. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  267. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  268. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  269. data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
  270. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  271. data/lib/hecks/ports/persistence.rb +36 -1
  272. data/lib/hecks/ports/projection.rb +61 -7
  273. data/lib/hecks/ports/query/in_memory.rb +41 -8
  274. data/lib/hecks/ports/query/ordering.rb +21 -6
  275. data/lib/hecks/ports/query.rb +35 -0
  276. data/lib/hecks/ports.rb +1 -0
  277. data/lib/hecks/projections/bootstrap_table.rb +147 -0
  278. data/lib/hecks/projections/diagrams.rb +315 -79
  279. data/lib/hecks/projections/glossary/html.rb +91 -3
  280. data/lib/hecks/projections/glossary/markdown.rb +50 -3
  281. data/lib/hecks/projections/glossary/mermaid.rb +49 -1
  282. data/lib/hecks/projections/glossary/sections.rb +1 -1
  283. data/lib/hecks/projections/glossary/sentences.rb +110 -5
  284. data/lib/hecks/projections/glossary.rb +173 -31
  285. data/lib/hecks/projections/ir.rb +1 -1
  286. data/lib/hecks/projections/model/deviations.rb +62 -17
  287. data/lib/hecks/projections/model.rb +75 -21
  288. data/lib/hecks/projections/oidc.rb +45 -8
  289. data/lib/hecks/projections/parser_table.rb +53 -12
  290. data/lib/hecks/projections/reference.rb +15 -3
  291. data/lib/hecks/projections/rust_vocabulary.rb +646 -0
  292. data/lib/hecks/projections/shape.rb +10 -2
  293. data/lib/hecks/projections/statements.rb +69 -22
  294. data/lib/hecks/projections/vocabulary.rb +26 -9
  295. data/lib/hecks/projections.rb +5 -3
  296. data/lib/hecks/projector/cli_projector.rb +245 -37
  297. data/lib/hecks/projector/docs_projector.rb +154 -28
  298. data/lib/hecks/projector/exporter.rb +104 -29
  299. data/lib/hecks/projector/ir_projector.rb +7 -1
  300. data/lib/hecks/projector/narrate_projector.rb +144 -23
  301. data/lib/hecks/projector/target.rb +42 -18
  302. data/lib/hecks/projector.rb +86 -18
  303. data/lib/hecks/query_ir.rb +94 -47
  304. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  305. data/lib/hecks/query_specification/common/comparison.rb +109 -27
  306. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  307. data/lib/hecks/query_specification/common/null_policy.rb +61 -15
  308. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  309. data/lib/hecks/query_specification/common/options.rb +25 -0
  310. data/lib/hecks/query_specification/field_path.rb +69 -15
  311. data/lib/hecks/query_specification/hop_path.rb +57 -20
  312. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  313. data/lib/hecks/rendering.rb +9 -3
  314. data/lib/hecks/router/namespace_installer.rb +16 -3
  315. data/lib/hecks/router.rb +56 -1
  316. data/lib/hecks/runtime/aggregate_lock.rb +20 -11
  317. data/lib/hecks/runtime/boot_gates.rb +21 -3
  318. data/lib/hecks/runtime/caller.rb +40 -8
  319. data/lib/hecks/runtime/capability_graph.rb +13 -2
  320. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
  321. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
  322. data/lib/hecks/runtime/command_interpreter.rb +195 -113
  323. data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
  324. data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
  325. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  326. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  327. data/lib/hecks/runtime/command_rules/references.rb +145 -55
  328. data/lib/hecks/runtime/command_rules.rb +3 -0
  329. data/lib/hecks/runtime/dependency_planning.rb +56 -11
  330. data/lib/hecks/runtime/dispatcher.rb +263 -92
  331. data/lib/hecks/runtime/entity_element.rb +304 -67
  332. data/lib/hecks/runtime/entity_interpreter.rb +149 -88
  333. data/lib/hecks/runtime/errors.rb +37 -23
  334. data/lib/hecks/runtime/event.rb +15 -10
  335. data/lib/hecks/runtime/identity.rb +91 -23
  336. data/lib/hecks/runtime/instance.rb +103 -18
  337. data/lib/hecks/runtime/interpreting.rb +24 -16
  338. data/lib/hecks/runtime/invocation.rb +358 -0
  339. data/lib/hecks/runtime/loader.rb +105 -19
  340. data/lib/hecks/runtime/outbox.rb +164 -26
  341. data/lib/hecks/runtime/policy_interpreter.rb +73 -60
  342. data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
  343. data/lib/hecks/runtime/query_interpreter.rb +93 -74
  344. data/lib/hecks/runtime/reaction_invocation.rb +73 -28
  345. data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
  346. data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
  347. data/lib/hecks/runtime/reference_hop.rb +48 -6
  348. data/lib/hecks/runtime/refusal_wording.rb +142 -115
  349. data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
  350. data/lib/hecks/runtime/registry/verification.rb +153 -28
  351. data/lib/hecks/runtime/registry.rb +202 -32
  352. data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
  353. data/lib/hecks/runtime/routing.rb +36 -154
  354. data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
  355. data/lib/hecks/runtime/saga_interpreter.rb +90 -76
  356. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  357. data/lib/hecks/runtime/tenant_check.rb +33 -13
  358. data/lib/hecks/runtime/tenant_scope.rb +23 -5
  359. data/lib/hecks/runtime/value/admission.rb +75 -30
  360. data/lib/hecks/runtime/value/coercion.rb +555 -142
  361. data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
  362. data/lib/hecks/runtime/value.rb +71 -21
  363. data/lib/hecks/runtime.rb +39 -7
  364. data/lib/hecks/storehouse.rb +368 -72
  365. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  366. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  367. data/lib/hecks/version.rb +3 -3
  368. data/lib/hecks/vocabulary.rb +205 -4
  369. data/lib/hecks.rb +91 -11
  370. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
  371. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
  372. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
  373. metadata +28 -2
@@ -16,9 +16,9 @@ module Hecks
16
16
 
17
17
  # Six operators, reduced to two primitives (less_than, equal) combined
18
18
  # with a small boolean algebra: compares_less_than/compares_equal choose
19
- # which primitive(s) OR together, negated inverts the result.
19
+ # which primitive(s) or together, negated inverts the result.
20
20
  #
21
- # READ, NOT RESTATED. This table is the checked-in projection of the
21
+ # **Read, not restated**. This table is the checked-in projection of the
22
22
  # grammar chapter's admitted set (bin/expression_projection), joined with
23
23
  # the algebra Vocabulary::Comparison declares. The evaluator cannot
24
24
  # boot the chapter that configures it — the Prism adapter normalises
@@ -53,20 +53,37 @@ 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
67
84
 
68
- # THE RULE-SHAPED ENTRY — evaluates a Given/Invariant (anything
69
- # answering `canonical` and `ast`) by walking its STRUCTURED form,
85
+ # The rule-shaped entry — evaluates a Given/Invariant (anything
86
+ # answering `canonical` and `ast`) by walking its structured form,
70
87
  # never re-parsing the text: the one parse happened at DSL-build
71
88
  # time behind `AstJson`, and `AstReader` turns that tree back into
72
89
  # the same nodes `parse` would have built (the equivalence is
@@ -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
 
@@ -122,18 +174,18 @@ module Hecks
122
174
  left, right = split_top_level(expr, "&&")
123
175
  return And.new(left: parse(left), right: parse(right)) if left
124
176
 
125
- # Tried BEFORE `.include?`/comparisons, not after — `!` negates
126
- # the WHOLE boolean expression that follows it (`!names.include?(x)`
177
+ # Tried before `.include?`/comparisons, not after — `!` negates
178
+ # the whole boolean expression that follows it (`!names.include?(x)`
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)
@@ -181,14 +260,29 @@ module Hecks
181
260
 
182
261
  # The algebra itself, on values already resolved — split out so a sign
183
262
  # test (SignTest#compares_via names an Operator symbol) can apply the
184
- # SAME primitives compare() uses against the literal 0, rather than
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,26 +307,42 @@ 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
217
326
 
218
- # THE SAME MIS-SPLIT `Resolver.match_call` had (its own comment
327
+ # The same mis-split `Resolver.match_call` had (its own comment
219
328
  # has the full story), found here too by the same generator: a
220
- # `.include?` needle can itself be — or contain — ANOTHER
329
+ # `.include?` needle can itself be — or contain — another
221
330
  # `.include?` call (`"".include?(arr.all? { |el| "".include?("")
222
331
  # }.to_s)`, a String built via `.to_s` off a block predicate
223
332
  # whose own body happens to include one) — `rindex` finds the
224
- # INNERMOST occurrence, not the outermost this split actually
333
+ # innermost occurrence, not the outermost this split actually
225
334
  # needs. Fixed identically: try each occurrence left to right,
226
335
  # keep the first whose own balanced-paren match reaches the
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
@@ -311,13 +488,13 @@ module Hecks
311
488
  # `{`/`}` depth -- vendored addition, not (yet) upstream
312
489
  # hecks (migration plan task 9): this method already
313
490
  # treats `(`/`)` as a grouping construct so an operator
314
- # INSIDE a call's parens is never mistaken for a top-level
491
+ # inside a call's parens is never mistaken for a top-level
315
492
  # split point ; `{`/`}` needed the identical treatment the
316
493
  # moment `Bluebook::Expression::Resolver` grew block-taking
317
494
  # `.all?`/`.any?`/`.none? { |s| PREDICATE }` support (see
318
495
  # resolver.rb's own `BlockPredicate` addition) -- without
319
- # this, an operator INSIDE the block's own predicate (e.g.
320
- # `s.length > 0`) reads as a top-level split of the WHOLE
496
+ # this, an operator inside the block's own predicate (e.g.
497
+ # `s.length > 0`) reads as a top-level split of the whole
321
498
  # `value.split("::").all? { |s| s.length > 0 }` expression,
322
499
  # confirmed live via `Lexicon::Lexicon.Lookup`/`Query::Query.
323
500
  # Run` (the exact `Phrase` invariant this gap was found
@@ -329,16 +506,17 @@ module Hecks
329
506
  # a quoted literal either, so this sits beside the existing
330
507
  # paren-depth branch, not instead of it.
331
508
  #
332
- # `[`/`]` -- the identical lesson a THIRD time (found live via
509
+ # `[`/`]` -- the identical lesson a third time (found live via
333
510
  # the type-directed bounded-exhaustive expression generator,
334
511
  # Phase 7 of the equivalence-gap plan): `Resolver::ArrayLiteral`
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)
@@ -1,7 +1,7 @@
1
- # HAND-WRITTEN — the block-predicate/find family of the leaf grammar
1
+ # **Hand-written** — the block-predicate/find family of the leaf grammar
2
2
  # (`Bluebook::Expression::Resolver`'s own `.all?`/`.any?`/`.none?`/
3
3
  # `.find` suffixes), split into this sibling file to keep resolver.rb
4
- # under Metrics/ModuleLength (350) : this reopens the SAME `Resolver`
4
+ # under Metrics/ModuleLength (350) : this reopens the same `Resolver`
5
5
  # module resolver.rb defines, the long nested `module A; module B; ...`
6
6
  # form (not the compact `A::B` form) so bare constant/method lookups
7
7
  # here (`EvaluationError`, `Evaluator`, `describe`, `unwrap_scalar`,
@@ -12,7 +12,7 @@
12
12
  module Hecks
13
13
  module Bluebook
14
14
  module Expression
15
- # Reopens the SAME `Resolver` module resolver.rb defines — see this
15
+ # Reopens the same `Resolver` module resolver.rb defines — see this
16
16
  # file's own header comment above for why the block-predicate/find
17
17
  # family lives here rather than in resolver.rb itself.
18
18
  module Resolver
@@ -23,12 +23,12 @@ module Hecks
23
23
  # (`value.split("::").all? { |s| s.length > 0 }`). Structurally
24
24
  # different from every other addition in this file: every prior
25
25
  # suffix is a flat receiver -> scalar transform, but a block
26
- # predicate needs to evaluate its own sub-expression ONCE PER
27
- # ELEMENT with the block parameter bound to that element. Kept
26
+ # predicate needs to evaluate its own sub-expression once per
27
+ # element with the block parameter bound to that element. Kept
28
28
  # minimal per the migration plan's own instruction -- no
29
29
  # persistent iteration-variable concept added to Resolver's
30
30
  # state model at all ; `predicate` below is a fully-parsed
31
- # EVALUATOR ast (not a Resolver ast -- the predicate is a
31
+ # evaluator ast (not a Resolver ast -- the predicate is a
32
32
  # boolean/comparison expression like `s.length > 0`, exactly the
33
33
  # grammar `Bluebook::Expression::Evaluator` owns, not this
34
34
  # module's own leaf grammar), parsed once at `parse`-time same as
@@ -78,7 +78,7 @@ module Hecks
78
78
 
79
79
  # Every suffix that opens a `{ |x| ... }` block, `.find` included
80
80
  # -- shared by `parse_block_opener` below, and by nothing else
81
- # (this is NOT `BLOCK_PREDICATE_MODES` — `.find` isn't a mode
81
+ # (this is not `BLOCK_PREDICATE_MODES` — `.find` isn't a mode
82
82
  # `evaluate_block_predicate` aggregates through, it builds a
83
83
  # `Find` node instead, see below).
84
84
  BLOCK_OPENER_SUFFIXES = (BLOCK_PREDICATE_MODES.keys + ["find"]).freeze
@@ -88,27 +88,27 @@ module Hecks
88
88
  # last among the suffix rules (right before the `Lookup`
89
89
  # catch-all) since a block's own predicate text can itself
90
90
  # contain almost anything a leaf expression can, including
91
- # ANOTHER block-opening suffix -- letting every more specific
91
+ # another block-opening suffix -- letting every more specific
92
92
  # rule above try first avoids this one accidentally swallowing a
93
93
  # receiver another rule was meant to parse.
94
94
  #
95
- # ONE combined header regex over ALL FOUR suffixes together,
95
+ # One combined header regex over all four suffixes together,
96
96
  # not `.find` and `.all?/any?/none?` scanned separately (that
97
97
  # was this file's own first cut, and it broke the moment a
98
- # block predicate's own predicate text contained a DIFFERENT
98
+ # block predicate's own predicate text contained a different
99
99
  # kind of block-opener than the one being scanned for --
100
100
  # `legs.any? { |leg| ... legs.find { |o| ... } ... }` : scanning
101
- # for `.find` FIRST found the INNER `.find`, not the outer
101
+ # for `.find` first found the inner `.find`, not the outer
102
102
  # `.any?`, because a non-greedy receiver capture only guarantees
103
- # the FIRST occurrence of ITS OWN suffix, not the first
104
- # occurrence of ANY block-opening suffix -- confirmed live via
103
+ # the first occurrence of its own suffix, not the first
104
+ # occurrence of any block-opening suffix -- confirmed live via
105
105
  # the shipping domain's own re-routing rules, the same
106
106
  # "no implicit conversion of Symbol into Integer" signature the
107
107
  # original nested-`.any?` bug had, not inferred). Scanning for
108
- # all four AT ONCE and letting the regex engine's own leftmost
109
- # match win fixes both directions (`.find` nested in `.any?` OR
110
- # `.any?` nested in `.find`) with the SAME one rule, since the
111
- # true receiver never itself contains ANY of these four words
108
+ # all four at once and letting the regex engine's own leftmost
109
+ # match win fixes both directions (`.find` nested in `.any?` or
110
+ # `.any?` nested in `.find`) with the same one rule, since the
111
+ # true receiver never itself contains any of these four words
112
112
  # followed by `{`.
113
113
  #
114
114
  # `matching_brace` walks forward counting `{`/`}` depth from
@@ -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
@@ -210,7 +238,7 @@ module Hecks
210
238
  # comment above. Reuses `interpret_with_element` unchanged
211
239
  # (below, shared with `BlockPredicate` — both bind `node.param`
212
240
  # to one element and interpret `node.predicate` against it) to
213
- # find the FIRST element the predicate accepts, then projects
241
+ # find the first element the predicate accepts, then projects
214
242
  # `node.path` through it via `walk_path`, the same dotted-
215
243
  # segment walk `lookup` uses for a plain attribute path. `nil`
216
244
  # (no matching element, or a `path` segment that doesn't
@@ -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