hecks 0.2.0 → 1.0.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 (419) hide show
  1. checksums.yaml +5 -5
  2. data/lib/hecks/adapters/driven/claude_code.adapter +3 -0
  3. data/lib/hecks/adapters/driven/claude_code.rb +127 -0
  4. data/lib/hecks/adapters/driven/d1.adapter +12 -0
  5. data/lib/hecks/adapters/driven/d1.rb +427 -0
  6. data/lib/hecks/adapters/driven/folder.adapter +3 -0
  7. data/lib/hecks/adapters/driven/folder.rb +199 -0
  8. data/lib/hecks/adapters/driven/google_authentication.adapter +3 -0
  9. data/lib/hecks/adapters/driven/google_authentication.rb +100 -0
  10. data/lib/hecks/adapters/driven/governance_authorization.adapter +3 -0
  11. data/lib/hecks/adapters/driven/governance_authorization.rb +101 -0
  12. data/lib/hecks/adapters/driven/heki/journal.rb +56 -0
  13. data/lib/hecks/adapters/driven/heki/saga_store.rb +99 -0
  14. data/lib/hecks/adapters/driven/heki/snapshot.rb +65 -0
  15. data/lib/hecks/adapters/driven/heki.adapter +4 -0
  16. data/lib/hecks/adapters/driven/heki.rb +179 -0
  17. data/lib/hecks/adapters/driven/identity_registry.adapter +3 -0
  18. data/lib/hecks/adapters/driven/identity_registry.rb +26 -0
  19. data/lib/hecks/adapters/driven/in_memory_ordering.rb +51 -0
  20. data/lib/hecks/adapters/driven/lambda/client.rb +63 -0
  21. data/lib/hecks/adapters/driven/lambda.adapter +4 -0
  22. data/lib/hecks/adapters/driven/lambda.rb +145 -0
  23. data/lib/hecks/adapters/driven/memory.adapter +3 -0
  24. data/lib/hecks/adapters/driven/memory.rb +103 -0
  25. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +3 -0
  26. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +28 -0
  27. data/lib/hecks/adapters/driven/postgres/codec.rb +88 -0
  28. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +207 -0
  29. data/lib/hecks/adapters/driven/postgres.adapter +5 -0
  30. data/lib/hecks/adapters/driven/postgres.rb +438 -0
  31. data/lib/hecks/adapters/driven/postgres_era.adapter +17 -0
  32. data/lib/hecks/adapters/driven/prism.adapter +3 -0
  33. data/lib/hecks/adapters/driven/prism.rb +80 -0
  34. data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
  35. data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
  36. data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
  37. data/lib/hecks/adapters/driven/sqlite/codec.rb +80 -0
  38. data/lib/hecks/adapters/driven/sqlite/projection.rb +172 -0
  39. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +192 -0
  40. data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
  41. data/lib/hecks/adapters/driven/sqlite.rb +306 -0
  42. data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
  43. data/lib/hecks/adapters/driven/system_clock.rb +14 -0
  44. data/lib/hecks/adapters/driven.rb +54 -0
  45. data/lib/hecks/adapters.rb +6 -0
  46. data/lib/hecks/behaviors/dsl.rb +125 -0
  47. data/lib/hecks/behaviors/expectations.rb +340 -0
  48. data/lib/hecks/behaviors/ir.rb +31 -0
  49. data/lib/hecks/behaviors/rspec.rb +42 -0
  50. data/lib/hecks/behaviors/runner.rb +96 -0
  51. data/lib/hecks/behaviors.rb +25 -0
  52. data/lib/hecks/bluebook/aggregate.rb +108 -0
  53. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
  54. data/lib/hecks/bluebook/assembly/build.rb +48 -0
  55. data/lib/hecks/bluebook/assembly/contract.rb +112 -0
  56. data/lib/hecks/bluebook/assembly/contracts.rb +438 -0
  57. data/lib/hecks/bluebook/assembly/marks.rb +228 -0
  58. data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
  59. data/lib/hecks/bluebook/assembly.rb +91 -0
  60. data/lib/hecks/bluebook/attribute.rb +96 -0
  61. data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
  62. data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
  63. data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
  64. data/lib/hecks/bluebook/behaviour/command.rb +116 -0
  65. data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
  66. data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
  67. data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
  68. data/lib/hecks/bluebook/behaviour/lifecycle.rb +68 -0
  69. data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
  70. data/lib/hecks/bluebook/behaviour/process_manager.rb +51 -0
  71. data/lib/hecks/bluebook/behaviour/query.rb +10 -0
  72. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
  73. data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
  74. data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
  75. data/lib/hecks/bluebook/chapter.rb +78 -0
  76. data/lib/hecks/bluebook/command.rb +124 -0
  77. data/lib/hecks/bluebook/domain_port.rb +102 -0
  78. data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
  79. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +1018 -0
  80. data/lib/hecks/bluebook/dsl/attribute_collector.rb +348 -0
  81. data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
  82. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1087 -0
  83. data/lib/hecks/bluebook/dsl/command_builder.rb +714 -0
  84. data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
  85. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
  86. data/lib/hecks/bluebook/dsl/entity_builder.rb +430 -0
  87. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
  88. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
  89. data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
  90. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
  91. data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
  92. data/lib/hecks/bluebook/dsl/policy_builder.rb +135 -0
  93. data/lib/hecks/bluebook/dsl/port_builder.rb +39 -0
  94. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
  95. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +306 -0
  96. data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
  97. data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
  98. data/lib/hecks/bluebook/dsl/rule_reference.rb +173 -0
  99. data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
  100. data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
  101. data/lib/hecks/bluebook/dsl/word_gate.rb +228 -0
  102. data/lib/hecks/bluebook/dsl/world_builder.rb +117 -0
  103. data/lib/hecks/bluebook/dsl.rb +45 -0
  104. data/lib/hecks/bluebook/entity.rb +103 -0
  105. data/lib/hecks/bluebook/expression/canonical_form.rb +123 -0
  106. data/lib/hecks/bluebook/expression/evaluator.rb +304 -0
  107. data/lib/hecks/bluebook/expression/projection.json +218 -0
  108. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
  109. data/lib/hecks/bluebook/expression/resolver.rb +781 -0
  110. data/lib/hecks/bluebook/expression.rb +13 -0
  111. data/lib/hecks/bluebook/hexagon.rb +60 -0
  112. data/lib/hecks/bluebook/lifecycle.rb +42 -0
  113. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
  114. data/lib/hecks/bluebook/meta_validator/judge.rb +621 -0
  115. data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
  116. data/lib/hecks/bluebook/meta_validator/port_judge.rb +51 -0
  117. data/lib/hecks/bluebook/meta_validator/readings.rb +360 -0
  118. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +351 -0
  119. data/lib/hecks/bluebook/meta_validator/shapes.rb +266 -0
  120. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
  121. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
  122. data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
  123. data/lib/hecks/bluebook/meta_validator.rb +542 -0
  124. data/lib/hecks/bluebook/model_check.rb +445 -0
  125. data/lib/hecks/bluebook/pattern_subset.rb +184 -0
  126. data/lib/hecks/bluebook/policy.rb +41 -0
  127. data/lib/hecks/bluebook/process_manager.rb +128 -0
  128. data/lib/hecks/bluebook/project_discovery.rb +30 -0
  129. data/lib/hecks/bluebook/project_loader.rb +40 -0
  130. data/lib/hecks/bluebook/project_register.rb +82 -0
  131. data/lib/hecks/bluebook/query.rb +61 -0
  132. data/lib/hecks/bluebook/read_model.rb +109 -0
  133. data/lib/hecks/bluebook/reference.rb +74 -0
  134. data/lib/hecks/bluebook/smoke_test.rb +166 -0
  135. data/lib/hecks/bluebook/synthesizer.rb +95 -0
  136. data/lib/hecks/bluebook/translation.rb +92 -0
  137. data/lib/hecks/bluebook/value_object.rb +66 -0
  138. data/lib/hecks/bluebook.rb +74 -0
  139. data/lib/hecks/codemod.rb +342 -0
  140. data/lib/hecks/construct.rb +71 -0
  141. data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
  142. data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
  143. data/lib/hecks/deploy/oidc.json +18 -0
  144. data/lib/hecks/doc/reference.rb +410 -0
  145. data/lib/hecks/embryonaut_bluebook.rb +75 -0
  146. data/lib/hecks/facade/cli_door.rb +119 -0
  147. data/lib/hecks/facade/cli_runner.rb +190 -0
  148. data/lib/hecks/facade/command_request.rb +105 -0
  149. data/lib/hecks/facade/handle.rb +173 -0
  150. data/lib/hecks/facade/json_door.rb +166 -0
  151. data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
  152. data/lib/hecks/facade/surface/chapter.rb +107 -0
  153. data/lib/hecks/facade/surface.rb +48 -0
  154. data/lib/hecks/facade.rb +44 -0
  155. data/lib/hecks/forms/app.rb +341 -0
  156. data/lib/hecks/forms/command_form_renderer.rb +113 -0
  157. data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
  158. data/lib/hecks/forms/field_renderer.rb +177 -0
  159. data/lib/hecks/forms/field_shape.rb +232 -0
  160. data/lib/hecks/forms/html.rb +84 -0
  161. data/lib/hecks/forms/index_renderer.rb +35 -0
  162. data/lib/hecks/forms/page.rb +157 -0
  163. data/lib/hecks/forms/params.rb +161 -0
  164. data/lib/hecks/forms/port_argument.rb +46 -0
  165. data/lib/hecks/forms/query_form_renderer.rb +114 -0
  166. data/lib/hecks/forms/record_renderer.rb +119 -0
  167. data/lib/hecks/forms/record_table.rb +68 -0
  168. data/lib/hecks/forms/reference_options.rb +30 -0
  169. data/lib/hecks/forms/value_object_shape.rb +46 -0
  170. data/lib/hecks/forms.rb +54 -0
  171. data/lib/hecks/fqn.rb +94 -0
  172. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
  173. data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
  174. data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
  175. data/lib/hecks/framework/bluebook/governance.bluebook +145 -0
  176. data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
  177. data/lib/hecks/framework/oidc.json +39 -0
  178. data/lib/hecks/framework.rb +90 -0
  179. data/lib/hecks/freezer.rb +67 -0
  180. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +527 -0
  181. data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
  182. data/lib/hecks/fuzzing/isolated_boot.rb +286 -0
  183. data/lib/hecks/fuzzing/properties.rb +1192 -0
  184. data/lib/hecks/fuzzing/replay.rb +668 -0
  185. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
  186. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +80 -0
  187. data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
  188. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +250 -0
  189. data/lib/hecks/fuzzing/sequence_generator.rb +131 -0
  190. data/lib/hecks/fuzzing/value_generator.rb +200 -0
  191. data/lib/hecks/fuzzing.rb +12 -0
  192. data/lib/hecks/grammar/evolve.rb +324 -0
  193. data/lib/hecks/grammar/expression.bluebook +420 -0
  194. data/lib/hecks/grammar/expression_operators.json +1648 -0
  195. data/lib/hecks/grammar/grammar.hecksagon +20 -0
  196. data/lib/hecks/grammar/oidc.json +69 -0
  197. data/lib/hecks/grammar/translation.bluebook +203 -0
  198. data/lib/hecks/grammar.rb +178 -0
  199. data/lib/hecks/ir.rb +126 -0
  200. data/lib/hecks/language/adapter.bluebook +116 -0
  201. data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
  202. data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
  203. data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
  204. data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
  205. data/lib/hecks/language/bluebook/command.bluebook +471 -0
  206. data/lib/hecks/language/bluebook/entity.bluebook +392 -0
  207. data/lib/hecks/language/bluebook/policy.bluebook +189 -0
  208. data/lib/hecks/language/bluebook/process_manager.bluebook +381 -0
  209. data/lib/hecks/language/bluebook/projection.bluebook +267 -0
  210. data/lib/hecks/language/bluebook/query.bluebook +245 -0
  211. data/lib/hecks/language/bluebook/shape.bluebook +292 -0
  212. data/lib/hecks/language/bluebook/syntax.bluebook +448 -0
  213. data/lib/hecks/language/bluebook/vocabulary.bluebook +379 -0
  214. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
  215. data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
  216. data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
  217. data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
  218. data/lib/hecks/language/oidc.json +333 -0
  219. data/lib/hecks/language/port.bluebook +120 -0
  220. data/lib/hecks/language/translation/translation.bluebook +110 -0
  221. data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
  222. data/lib/hecks/language/world/wiring.bluebook +62 -0
  223. data/lib/hecks/language/world/world.bluebook +84 -0
  224. data/lib/hecks/literal.rb +125 -0
  225. data/lib/hecks/naming.rb +174 -0
  226. data/lib/hecks/ports/access_control.port +9 -0
  227. data/lib/hecks/ports/access_control.rb +62 -0
  228. data/lib/hecks/ports/agent/answers.rb +104 -0
  229. data/lib/hecks/ports/agent.port +8 -0
  230. data/lib/hecks/ports/agent.rb +167 -0
  231. data/lib/hecks/ports/authentication.port +6 -0
  232. data/lib/hecks/ports/authentication.rb +50 -0
  233. data/lib/hecks/ports/authorization.port +7 -0
  234. data/lib/hecks/ports/authorization.rb +62 -0
  235. data/lib/hecks/ports/clock.port +5 -0
  236. data/lib/hecks/ports/clock.rb +62 -0
  237. data/lib/hecks/ports/extraction.port +5 -0
  238. data/lib/hecks/ports/extraction.rb +37 -0
  239. data/lib/hecks/ports/identity_assignment.port +5 -0
  240. data/lib/hecks/ports/identity_assignment.rb +45 -0
  241. data/lib/hecks/ports/identity_generation.port +5 -0
  242. data/lib/hecks/ports/identity_generation.rb +49 -0
  243. data/lib/hecks/ports/identity_resolution.port +5 -0
  244. data/lib/hecks/ports/identity_resolution.rb +40 -0
  245. data/lib/hecks/ports/loading.port +4 -0
  246. data/lib/hecks/ports/loading.rb +13 -0
  247. data/lib/hecks/ports/persistence/append_only.rb +138 -0
  248. data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
  249. data/lib/hecks/ports/persistence/execution.rb +23 -0
  250. data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
  251. data/lib/hecks/ports/persistence/plugin.rb +54 -0
  252. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +169 -0
  253. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +128 -0
  254. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +161 -0
  255. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +61 -0
  256. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +304 -0
  257. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +171 -0
  258. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +190 -0
  259. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +478 -0
  260. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +166 -0
  261. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +314 -0
  262. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +168 -0
  263. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +170 -0
  264. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +134 -0
  265. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +137 -0
  266. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +89 -0
  267. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +85 -0
  268. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
  269. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +132 -0
  270. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +74 -0
  271. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +758 -0
  272. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +120 -0
  273. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +31 -0
  274. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +46 -0
  275. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +102 -0
  276. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +46 -0
  277. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +70 -0
  278. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +72 -0
  279. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +120 -0
  280. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +183 -0
  281. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +41 -0
  282. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +28 -0
  283. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +30 -0
  284. data/lib/hecks/ports/persistence/plugins/era/translation.rb +11 -0
  285. data/lib/hecks/ports/persistence/plugins/era.rb +48 -0
  286. data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
  287. data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
  288. data/lib/hecks/ports/persistence.port +4 -0
  289. data/lib/hecks/ports/persistence.rb +39 -0
  290. data/lib/hecks/ports/projection.port +4 -0
  291. data/lib/hecks/ports/projection.rb +96 -0
  292. data/lib/hecks/ports/query/in_memory.rb +60 -0
  293. data/lib/hecks/ports/query/ordering.rb +41 -0
  294. data/lib/hecks/ports/query.rb +36 -0
  295. data/lib/hecks/ports.rb +27 -0
  296. data/lib/hecks/projections/diagrams.rb +642 -0
  297. data/lib/hecks/projections/ir.rb +18 -0
  298. data/lib/hecks/projections/model/deviations.rb +98 -0
  299. data/lib/hecks/projections/model.rb +145 -0
  300. data/lib/hecks/projections/oidc.rb +110 -0
  301. data/lib/hecks/projections/parser_table.rb +159 -0
  302. data/lib/hecks/projections/reference.rb +38 -0
  303. data/lib/hecks/projections/shape.rb +44 -0
  304. data/lib/hecks/projections/statements.rb +110 -0
  305. data/lib/hecks/projections/vocabulary.rb +100 -0
  306. data/lib/hecks/projections.rb +32 -0
  307. data/lib/hecks/projector/cli_projector.rb +446 -0
  308. data/lib/hecks/projector/docs_projector.rb +321 -0
  309. data/lib/hecks/projector/exporter.rb +158 -0
  310. data/lib/hecks/projector/ir_projector.rb +18 -0
  311. data/lib/hecks/projector/narrate_projector.rb +243 -0
  312. data/lib/hecks/projector/target.rb +97 -0
  313. data/lib/hecks/projector.rb +186 -0
  314. data/lib/hecks/query_ir.rb +411 -0
  315. data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
  316. data/lib/hecks/query_specification/common/comparators.rb +25 -0
  317. data/lib/hecks/query_specification/common/comparison.rb +174 -0
  318. data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
  319. data/lib/hecks/query_specification/common/dsl.rb +58 -0
  320. data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
  321. data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
  322. data/lib/hecks/query_specification/common/null_policy.rb +92 -0
  323. data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
  324. data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
  325. data/lib/hecks/query_specification/common/options.rb +32 -0
  326. data/lib/hecks/query_specification/common/order_by.rb +9 -0
  327. data/lib/hecks/query_specification/common/specification.rb +10 -0
  328. data/lib/hecks/query_specification/common/where_clause.rb +9 -0
  329. data/lib/hecks/query_specification/field_path.rb +107 -0
  330. data/lib/hecks/query_specification/hop_path.rb +132 -0
  331. data/lib/hecks/query_specification/read_model/specification.rb +18 -0
  332. data/lib/hecks/query_specification.rb +14 -0
  333. data/lib/hecks/rendering.rb +48 -0
  334. data/lib/hecks/router/namespace_installer.rb +157 -0
  335. data/lib/hecks/router.rb +70 -0
  336. data/lib/hecks/runtime/aggregate_lock.rb +45 -0
  337. data/lib/hecks/runtime/boot_gates.rb +41 -0
  338. data/lib/hecks/runtime/caller.rb +66 -0
  339. data/lib/hecks/runtime/capability_graph.rb +44 -0
  340. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +135 -0
  341. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +260 -0
  342. data/lib/hecks/runtime/command_interpreter.rb +497 -0
  343. data/lib/hecks/runtime/command_rules/admissibility.rb +357 -0
  344. data/lib/hecks/runtime/command_rules/arithmetic.rb +260 -0
  345. data/lib/hecks/runtime/command_rules/authorization.rb +65 -0
  346. data/lib/hecks/runtime/command_rules/emission.rb +34 -0
  347. data/lib/hecks/runtime/command_rules/references.rb +189 -0
  348. data/lib/hecks/runtime/command_rules.rb +28 -0
  349. data/lib/hecks/runtime/dependency_planning.rb +245 -0
  350. data/lib/hecks/runtime/dispatcher.rb +301 -0
  351. data/lib/hecks/runtime/entity_element.rb +253 -0
  352. data/lib/hecks/runtime/entity_interpreter.rb +299 -0
  353. data/lib/hecks/runtime/errors.rb +122 -0
  354. data/lib/hecks/runtime/event.rb +51 -0
  355. data/lib/hecks/runtime/identity.rb +156 -0
  356. data/lib/hecks/runtime/instance.rb +175 -0
  357. data/lib/hecks/runtime/interpreting.rb +90 -0
  358. data/lib/hecks/runtime/loader.rb +184 -0
  359. data/lib/hecks/runtime/policy_interpreter.rb +366 -0
  360. data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
  361. data/lib/hecks/runtime/query_interpreter.rb +286 -0
  362. data/lib/hecks/runtime/reaction_invocation.rb +253 -0
  363. data/lib/hecks/runtime/read_model_interpreter.rb +341 -0
  364. data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
  365. data/lib/hecks/runtime/reference_hop.rb +99 -0
  366. data/lib/hecks/runtime/refusal_wording.rb +126 -0
  367. data/lib/hecks/runtime/registry/saga_persistence.rb +142 -0
  368. data/lib/hecks/runtime/registry/verification.rb +246 -0
  369. data/lib/hecks/runtime/registry.rb +279 -0
  370. data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
  371. data/lib/hecks/runtime/routing.rb +96 -0
  372. data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
  373. data/lib/hecks/runtime/saga_interpreter.rb +503 -0
  374. data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -0
  375. data/lib/hecks/runtime/tenant_check.rb +84 -0
  376. data/lib/hecks/runtime/tenant_scope.rb +55 -0
  377. data/lib/hecks/runtime/value/admission.rb +130 -0
  378. data/lib/hecks/runtime/value/coercion.rb +565 -0
  379. data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
  380. data/lib/hecks/runtime/value.rb +125 -0
  381. data/lib/hecks/runtime.rb +107 -0
  382. data/lib/hecks/storehouse.rb +632 -0
  383. data/lib/hecks/version.rb +16 -0
  384. data/lib/hecks/vocabulary.rb +218 -0
  385. data/lib/hecks.rb +123 -6
  386. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
  387. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
  388. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
  389. metadata +412 -222
  390. data/bin/hecks +0 -7
  391. data/bin/hecks-package +0 -65
  392. data/bin/hecks_console +0 -12
  393. data/bin/hecks_serverless +0 -6
  394. data/lib/cli/build.rb +0 -14
  395. data/lib/cli/command_runner.rb +0 -28
  396. data/lib/cli/console.rb +0 -10
  397. data/lib/cli/generate.rb +0 -37
  398. data/lib/cli/hecks-cli.rb +0 -27
  399. data/lib/cli/test.rb +0 -57
  400. data/lib/console/commands.rb +0 -8
  401. data/lib/console/hecks-console.rb +0 -1
  402. data/lib/packager/README.md +0 -0
  403. data/lib/packager/app_runner.rb +0 -21
  404. data/lib/packager/args.rb +0 -26
  405. data/lib/packager/compatibility/fixnum.rb +0 -6
  406. data/lib/packager/hecks.rb +0 -39
  407. data/lib/packager/query_runner.rb +0 -21
  408. data/lib/packager/resources/Dockerfile +0 -11
  409. data/lib/packager/resources/app_binary +0 -7
  410. data/lib/packager/resources/bundle_config +0 -3
  411. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
  412. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
  413. data/lib/serverless/Domain +0 -32
  414. data/lib/serverless/cli.rb +0 -75
  415. data/lib/serverless/resources/command_name.js +0 -5
  416. data/lib/serverless/resources/environment.js +0 -7
  417. data/lib/serverless/resources/handler.js.tt +0 -28
  418. data/lib/serverless/resources/run_binary.js +0 -22
  419. data/lib/serverless/resources/serverless.yml +0 -20
@@ -0,0 +1,781 @@
1
+ require "json"
2
+ require_relative "../../rendering"
3
+ require_relative "../../vocabulary"
4
+ require_relative "resolver/block_predicates"
5
+
6
+ module Hecks
7
+ module Bluebook
8
+ module Expression
9
+ class EvaluationError < StandardError; end
10
+
11
+ module Resolver
12
+ SIGN_TESTS = Hecks::Vocabulary.fetch("SignTest")
13
+
14
+ # Which Comparison operator each sign test is sugar for, against the
15
+ # literal 0 — declared the same way in Vocabulary::SignTest's
16
+ # compares_via (language/bluebook/vocabulary.bluebook) ; spec/vocabulary_conformance_spec
17
+ # holds the two tables equal.
18
+ SIGN_TEST_OPERATORS = {
19
+ "positive?" => ">",
20
+ "negative?" => "<",
21
+ "zero?" => "=="
22
+ }.freeze
23
+
24
+ # The leaf grammar an expression's dotted/arithmetic side parses into.
25
+ # Which node a string produces is a pure function of the string, so
26
+ # parse() runs once per distinct leaf (folded into Evaluator's own
27
+ # ast_cache — see evaluator.rb) and interpret() reads state/attrs
28
+ # fresh on every call. Only Lookup — the true leaf — ever touches
29
+ # state/attrs.
30
+ IntegerLiteral = Struct.new(:value, keyword_init: true)
31
+ FloatLiteral = Struct.new(:value, keyword_init: true)
32
+ StringLiteral = Struct.new(:value, keyword_init: true)
33
+ BoolLiteral = Struct.new(:value, keyword_init: true)
34
+ # `["active", "suspended"]` — a literal set, the haystack half of
35
+ # an `.include?`. Vocabulary::IncludeHaystack has always ADMITTED
36
+ # Array (and Evaluator#includes? has always had a `when Array`
37
+ # arm), but nothing could produce one: there was no array-literal
38
+ # node, so `["a", "b"].include?(x)` fell through to Lookup and
39
+ # refused with `cannot resolve "[\"a\", \"b\"]"`. A declared
40
+ # capability with no way to spell it.
41
+ ArrayLiteral = Struct.new(:elements, keyword_init: true)
42
+ # A plain class, not `Struct.new(keyword_init: true)` — every
43
+ # sibling leaf node here carries at least one field, but this
44
+ # one carries none by nature (a nil literal has no data to
45
+ # hold), and `Struct.new` with zero member names ahead of
46
+ # `keyword_init:` is real, live Ruby-version-dependent
47
+ # behavior: works on 3.3, raises "wrong number of arguments
48
+ # (given 0, expected 1+)" on 3.2 (caught deploying to Lambda's
49
+ # own ruby3.2 runtime). `.new`/`case ... when NilLiteral` below
50
+ # are the only two things this type is ever used for, and a
51
+ # bare class answers both identically.
52
+ NilLiteral = Class.new
53
+ Addition = Struct.new(:left, :right, keyword_init: true)
54
+ SignTest = Struct.new(:operator, :test, :receiver, keyword_init: true)
55
+ Empty = Struct.new(:receiver, keyword_init: true)
56
+ ToS = Struct.new(:receiver, keyword_init: true)
57
+ Modulo = Struct.new(:receiver, :divisor, keyword_init: true)
58
+ Size = Struct.new(:receiver, keyword_init: true)
59
+ Lookup = Struct.new(:path, keyword_init: true)
60
+
61
+ # UPDATE 2026-08-27: every "vendored addition, not (yet) upstream
62
+ # hecks" comment on this file's own MatchesRegex/Presence/Split/
63
+ # First/Last/StartsWith/EndsWith below (plus ArrayLiteral above)
64
+ # described a REAL gap, found the hard way, in the history each
65
+ # comment tells — and that history stays exactly as written,
66
+ # on purpose. What changed is the PRESENT TENSE claim "not (yet)
67
+ # upstream": a review of this exact migration found these eight
68
+ # symbols had working Ruby parse/interpret arms but had NEVER
69
+ # gone through Propose -> Render -> Admit
70
+ # (lib/hecks/grammar/expression_operators.json) the way every
71
+ # other operator here has — a closed-vocabulary guard
72
+ # (spec/operator_conformance_spec.rb) built entirely over TABLES
73
+ # structurally could not see hand-coded Struct/parse/interpret
74
+ # additions, so eight operators ran in Ruby, admitted nowhere,
75
+ # invisible to the one guard whose whole job was "reads in every
76
+ # target." All eight are now ledger-admitted for real, with full
77
+ # Rust kernel parity (rust/src/kernel/expression_operators/
78
+ # {pattern_match,presence,text,positional}.rs) — the two-tier
79
+ # gap is closed, not merely tracked.
80
+ #
81
+ # `receiver.match?(/pattern/)` -- vendored addition, not (yet)
82
+ # upstream hecks (migration plan task 8): confirmed the
83
+ # SINGLE most impactful corpus-wide dispatch-time gap of the
84
+ # whole migration -- `.match?(regex)` appears in nearly every
85
+ # value_object's format-validation rule across every corpus
86
+ # this migration touched (email/phone/ISO-8601-timestamp/zip
87
+ # patterns, dozens of files), and had NO parse support at all:
88
+ # it fell all the way through to the `Lookup` catch-all below,
89
+ # which tried to split the ENTIRE ".match?(/\A\d{5}\z/)" text
90
+ # on "." as if it were a dotted attribute path, and crashed with
91
+ # an opaque "no implicit conversion of Symbol into Integer"
92
+ # somewhere downstream -- confirmed live via a real dispatch,
93
+ # not validate (validate never evaluates a predicate body).
94
+ # `receiver` still needs its own parse (it may itself be a
95
+ # dotted lookup, e.g. `some_field.match?(...)`), the pattern
96
+ # text is taken as-is between the slashes (a Ruby Regexp literal,
97
+ # not the evaluator's own mini-grammar -- there is nothing to
98
+ # recurse into).
99
+ MatchesRegex = Struct.new(:receiver, :pattern, :flags, keyword_init: true)
100
+
101
+ # `.present?`/`.blank?` -- vendored addition, not (yet) upstream
102
+ # hecks (migration plan task 8): the second-most pervasive
103
+ # gap this pass found, right behind `.match?` -- confirmed real
104
+ # via the same "no such attribute" Lookup-fallback crash, since
105
+ # neither suffix existed in this leaf grammar at all. Rails-
106
+ # standard semantics (blank = nil, or responds_to?(:empty?) &&
107
+ # empty? ; present = !blank), not just `!nil?` -- matches what
108
+ # every corpus author actually means by `.present?` on a string
109
+ # field that could legitimately be "" rather than absent.
110
+ Presence = Struct.new(:receiver, :negated, keyword_init: true)
111
+
112
+ # `receiver.split("SEP")` -- vendored addition, not (yet) upstream
113
+ # hecks (migration plan task 9): confirmed the single
114
+ # highest-value new finding of the real-dispatch smoke sweep --
115
+ # the bus's own core `Phrase` value object (dispatch/lexicon/
116
+ # query/command_bus.bluebook, all four storehouse-kernel files,
117
+ # byte-identical text) validates its four-segment shape with
118
+ # `value.split("::").length == 4 && value.split("::").all? { |s|
119
+ # s.length > 0 }` -- `.split(` matched none of this grammar's
120
+ # known suffixes, so it fell through to the `Lookup` catch-all,
121
+ # which split the RAW EXPRESSION TEXT on "." (not the runtime
122
+ # value) and crashed with `TypeError: no implicit conversion of
123
+ # Symbol into Integer` the moment `String#[]` was handed a
124
+ # Symbol segment -- meaning every command taking a Phrase was
125
+ # entirely undispatchable, confirmed live via `Lexicon::Lexicon.
126
+ # Lookup`/`Query::Query.Run`, not inferred. Produces a real
127
+ # Array, deliberately not a scalar -- `.length`/`.all?` compose
128
+ # with it below the same way they compose with any other
129
+ # receiver, because `parse` recurses on the receiver text and
130
+ # the existing `Size` node (`.length`/`.size`) already treats
131
+ # "whatever `interpret` returns" as its receiver's value, not a
132
+ # fixed type. Separator is taken literally between the quotes,
133
+ # same "no sub-grammar to recurse into" precedent `MatchesRegex`
134
+ # already set for its own `/pattern/` text above.
135
+ Split = Struct.new(:receiver, :separator, keyword_init: true)
136
+
137
+ # `receiver.last` -- vendored addition, not (yet) upstream
138
+ # hecks (migration plan task 9), same pass as `Split` above
139
+ # and built for the same `Phrase`/`Path` value objects --
140
+ # `Query::Phrase`'s own invariant chains `.split("::").last.
141
+ # match?(...)` to check the fourth segment's casing. Same shape
142
+ # as `.length`/`.size` (Size) and `.empty?` (Empty) -- a plain
143
+ # receiver-in, scalar-out accessor, no sub-grammar of its own.
144
+ Last = Struct.new(:receiver, keyword_init: true)
145
+
146
+ # `receiver.first` -- sibling addition to `Last` immediately
147
+ # above, same shape, same vendored-not-upstream status. Added
148
+ # for the shipping domain's `legs.first` (an itinerary's
149
+ # departure leg), the exact mirror of `legs.last` (its arrival
150
+ # leg) that domain already leaned on -- `Last`'s own duck-typed-
151
+ # on-`respond_to?` reasoning applies unchanged, so `first_of`
152
+ # below is `last_of` with the one method swapped, not a new
153
+ # design.
154
+ First = Struct.new(:receiver, keyword_init: true)
155
+
156
+ # `receiver.start_with?("prefix")` / `receiver.end_with?("suffix")`
157
+ # -- vendored addition, not (yet) upstream hecks (migration
158
+ # plan task 9), the sibling gap the `Split`/`Last`/`BlockPredicate`
159
+ # pass above flagged and deliberately left unfixed --
160
+ # `Query::Query.Run`/`Dispatch::Dispatch.Route`/`CommandBus::
161
+ # CommandBus.Route`'s shared `Params` value object validates its
162
+ # JSON-object shape with `value.start_with?("{") && value.
163
+ # end_with?("}")` (dispatch/query/command_bus.bluebook, all three
164
+ # storehouse-kernel files, byte-identical text) -- `.start_with?(`/
165
+ # `.end_with?(` matched none of this grammar's known suffixes, so
166
+ # both fell through to the `Lookup` catch-all and crashed with the
167
+ # identical `TypeError: no implicit conversion of Symbol into
168
+ # Integer` shape `.split`/`.all?` used to, confirmed live via a
169
+ # real dispatch (not validate), not inferred. Two separate node
170
+ # types rather than one `mode:`-keyed struct (the `BlockPredicate`/
171
+ # `SignTest` precedent) -- `start_with?`/`end_with?` aren't two
172
+ # spellings of the same test the way `all?`/`any?`/`none?` are (one
173
+ # Array-aggregation family) or `positive?`/`negative?`/`zero?` are
174
+ # (one comparison-against-0 family) ; they check different ends of
175
+ # the same string and share nothing but their
176
+ # receiver-plus-literal-argument shape. `substring` is taken
177
+ # literally between the quotes, same "no sub-grammar to recurse
178
+ # into" precedent `Split`'s `separator`/`MatchesRegex`'s `pattern`
179
+ # already set.
180
+ StartsWith = Struct.new(:receiver, :substring, keyword_init: true)
181
+ EndsWith = Struct.new(:receiver, :substring, keyword_init: true)
182
+
183
+ module_function
184
+
185
+ def resolve(expr, state, attrs)
186
+ interpret(parse(expr), state, attrs)
187
+ end
188
+
189
+ def parse(expr)
190
+ expr = expr.to_s.strip
191
+
192
+ return Size.new(receiver: parse(Regexp.last_match(1))) if expr =~ /\A(.+)\.length\z/
193
+
194
+ return IntegerLiteral.new(value: Integer(expr, 10)) if expr.match?(/\A-?\d+\z/)
195
+ return FloatLiteral.new(value: Float(expr)) if expr.match?(/\A-?\d*\.\d+\z/)
196
+ return StringLiteral.new(value: expr[1..-2]) if quoted?(expr)
197
+ return BoolLiteral.new(value: true) if expr == "true"
198
+ return BoolLiteral.new(value: false) if expr == "false"
199
+ return NilLiteral.new if expr == "nil"
200
+
201
+ elements = array_elements(expr)
202
+ return ArrayLiteral.new(elements: elements.map { |element| parse(element) }) if elements
203
+
204
+ arithmetic = split_addition(expr)
205
+ return Addition.new(left: parse(arithmetic[0]), right: parse(arithmetic[1])) if arithmetic
206
+
207
+ sign = match_suffix(expr, SIGN_TESTS)
208
+ return sign_test_node(sign) if sign
209
+
210
+ return Empty.new(receiver: parse(Regexp.last_match(1))) if expr =~ /\A(.+)\.empty\?\z/
211
+ return ToS.new(receiver: parse(Regexp.last_match(1))) if expr =~ /\A(.+)\.to_s\z/
212
+
213
+ modulo = match_call(expr, ".modulo(")
214
+ return Modulo.new(receiver: parse(modulo[0]), divisor: parse(modulo[1])) if modulo
215
+
216
+ return Size.new(receiver: parse(Regexp.last_match(1))) if expr =~ /\A(.+)\.size\z/
217
+
218
+ if expr =~ /\A(.+)\.match\?\(\/(.*)\/([a-z]*)\)\z/m
219
+ return MatchesRegex.new(receiver: parse(Regexp.last_match(1)),
220
+ pattern: Regexp.last_match(2),
221
+ flags: Regexp.last_match(3))
222
+ end
223
+
224
+ return Presence.new(receiver: parse(Regexp.last_match(1)), negated: false) if expr =~ /\A(.+)\.present\?\z/
225
+ return Presence.new(receiver: parse(Regexp.last_match(1)), negated: true) if expr =~ /\A(.+)\.blank\?\z/
226
+
227
+ if expr =~ /\A(.+)\.split\("([^"]*)"\)\z/
228
+ return Split.new(receiver: parse(Regexp.last_match(1)), separator: Regexp.last_match(2))
229
+ end
230
+
231
+ return First.new(receiver: parse(Regexp.last_match(1))) if expr =~ /\A(.+)\.first\z/
232
+ return Last.new(receiver: parse(Regexp.last_match(1))) if expr =~ /\A(.+)\.last\z/
233
+
234
+ if expr =~ /\A(.+)\.start_with\?\("([^"]*)"\)\z/
235
+ return StartsWith.new(receiver: parse(Regexp.last_match(1)), substring: Regexp.last_match(2))
236
+ end
237
+
238
+ if expr =~ /\A(.+)\.end_with\?\("([^"]*)"\)\z/
239
+ return EndsWith.new(receiver: parse(Regexp.last_match(1)), substring: Regexp.last_match(2))
240
+ end
241
+
242
+ block_opener = parse_block_opener(expr)
243
+ return block_opener if block_opener
244
+
245
+ Lookup.new(path: expr)
246
+ end
247
+
248
+ def sign_test_node(parts)
249
+ receiver, test = parts
250
+ symbol = SIGN_TEST_OPERATORS.fetch(test)
251
+ operator = Evaluator::OPERATORS.find { |candidate| candidate.symbol == symbol }
252
+ SignTest.new(operator: operator, test: test, receiver: parse(receiver))
253
+ end
254
+
255
+ def interpret(node, state, attrs)
256
+ case node
257
+ when IntegerLiteral, FloatLiteral, StringLiteral, BoolLiteral then node.value
258
+ when ArrayLiteral then node.elements.map { |element| interpret(element, state, attrs) }
259
+ when NilLiteral then nil
260
+ when Addition
261
+ add(interpret(node.left, state, attrs), interpret(node.right, state, attrs))
262
+ when SignTest
263
+ apply_sign_test(node, interpret(node.receiver, state, attrs))
264
+ when Empty
265
+ emptiness_of(interpret(node.receiver, state, attrs))
266
+ when ToS
267
+ string_of(interpret(node.receiver, state, attrs))
268
+ when Modulo
269
+ apply_modulo(interpret(node.receiver, state, attrs), interpret(node.divisor, state, attrs))
270
+ when Size
271
+ size_of(interpret(node.receiver, state, attrs))
272
+ when MatchesRegex
273
+ matches_regex?(interpret(node.receiver, state, attrs), node.pattern, node.flags)
274
+ when Presence
275
+ present = !blank?(interpret(node.receiver, state, attrs))
276
+ node.negated ? !present : present
277
+ when Split
278
+ split_value(interpret(node.receiver, state, attrs), node.separator)
279
+ when Last
280
+ last_of(interpret(node.receiver, state, attrs))
281
+ when First
282
+ first_of(interpret(node.receiver, state, attrs))
283
+ when Find
284
+ found_of(node, interpret(node.receiver, state, attrs), state, attrs)
285
+ when StartsWith
286
+ starts_with?(interpret(node.receiver, state, attrs), node.substring)
287
+ when EndsWith
288
+ ends_with?(interpret(node.receiver, state, attrs), node.substring)
289
+ when BlockPredicate
290
+ evaluate_block_predicate(node, interpret(node.receiver, state, attrs), state, attrs)
291
+ when Lookup
292
+ lookup(node.path, state, attrs)
293
+ else
294
+ # Every leaf node `parse` can produce has a `when` above —
295
+ # a backstop against the day this grammar grows a new leaf
296
+ # type (this file's own history: MatchesRegex/Presence/
297
+ # Split/Last/First/Find/StartsWith/EndsWith/BlockPredicate
298
+ # were each added exactly this way, and each one — before it had an
299
+ # `interpret` arm — fell all the way through to the
300
+ # `Lookup` catch-all in `parse` and crashed downstream with
301
+ # an opaque type error, never here). A missing arm here
302
+ # would instead return bare `nil` silently, the one wrong-
303
+ # answer shape this leaf grammar has otherwise never
304
+ # allowed.
305
+ raise EvaluationError, "no interpreter handles #{node.class} — add a case before parse can produce it"
306
+ end
307
+ end
308
+
309
+ # `.present?`/`.blank?` -- vendored addition, see the
310
+ # `Presence` struct's own comment above. `nil` and `false` are
311
+ # blank ; a String/Array/Hash is blank when EMPTY, not merely
312
+ # falsy -- a VO-wrapped field that IS assigned (`{value: "x"}`,
313
+ # `Value#to_h`'d first) is present regardless of what its own
314
+ # inner value holds, matching how every VO-typed field in this
315
+ # corpus is actually shaped once set at all.
316
+ def blank?(value)
317
+ return true if value.nil? || value == false
318
+
319
+ # Duck-typed, not `value.is_a?(Runtime::Value)` -- this module
320
+ # is `Bluebook::Expression`, a different namespace tree from
321
+ # `Runtime::Value` entirely, and reaching across for a single
322
+ # class check is the exact cross-module coupling that already
323
+ # broke `Modulo`'s own `match_call` reference elsewhere in
324
+ # this file (found live while building this fix, not assumed).
325
+ value = value.to_h if value.respond_to?(:to_h) && !value.is_a?(Hash)
326
+ case value
327
+ when String, Array, Hash then value.empty?
328
+ else false
329
+ end
330
+ end
331
+
332
+ # `receiver.match?(/pattern/)` -- vendored addition, see the
333
+ # `MatchesRegex` struct's own comment above. `receiver_value` is
334
+ # coerced to a plain String first -- inlined here rather than
335
+ # calling `Evaluator#string_of` (a DIFFERENT module_function
336
+ # module; not actually in scope from inside Resolver despite
337
+ # `Modulo`'s own parse rule above calling a same-named
338
+ # `match_call` that has the identical cross-module problem --
339
+ # found live while building this, not assumed).
340
+ def matches_regex?(receiver_value, pattern, flags)
341
+ text = case receiver_value
342
+ when String, Symbol then receiver_value.to_s
343
+ when Integer, Float then receiver_value.to_s
344
+ when NilClass then ""
345
+ else
346
+ raise EvaluationError, "match? expects a scalar, got #{receiver_value.class}"
347
+ end
348
+
349
+ options = 0
350
+ options |= Regexp::IGNORECASE if flags.include?("i")
351
+ options |= Regexp::MULTILINE if flags.include?("m")
352
+ options |= Regexp::EXTENDED if flags.include?("x")
353
+
354
+ Regexp.new(pattern, options).match?(text)
355
+ rescue RegexpError => e
356
+ # M9: a malformed pattern between the slashes (an unclosed
357
+ # character class, say) is a defect in the EXPRESSION TEXT
358
+ # itself, exactly the same category of author mistake an
359
+ # unresolvable attribute name already refuses for — `Regexp.new`
360
+ # raising a raw `RegexpError` crossed this sublanguage's own
361
+ # refusal boundary the same way the `ZeroDivisionError`/
362
+ # `TypeError` cases elsewhere in this file did.
363
+ raise EvaluationError, "match? given an invalid pattern #{pattern.inspect} — #{e.message}"
364
+ end
365
+
366
+ # The elements of a bracketed literal, or nil if this isn't one.
367
+ # Splits on TOP-LEVEL commas only — quote-aware and depth-aware,
368
+ # the same discipline `split_addition` already applies, so a
369
+ # nested array or a comma inside a string element stays whole.
370
+ def array_elements(expr)
371
+ return nil unless expr.start_with?("[") && expr.end_with?("]")
372
+
373
+ inner = expr[1..-2].strip
374
+ return [] if inner.empty?
375
+
376
+ elements = []
377
+ depth = 0
378
+ quote = nil
379
+ current = +""
380
+ inner.each_char do |char|
381
+ if quote
382
+ quote = nil if char == quote
383
+ current << char
384
+ next
385
+ end
386
+ case char
387
+ when '"', "'" then quote = char
388
+ when "[", "(" then depth += 1
389
+ when "]", ")" then depth -= 1
390
+ end
391
+ if char == "," && depth.zero?
392
+ elements << current.strip
393
+ current = +""
394
+ else
395
+ current << char
396
+ end
397
+ end
398
+ elements << current.strip
399
+ elements.reject(&:empty?)
400
+ end
401
+
402
+ # BRACES COUNT TOWARD DEPTH, exactly as parens do — a `+` inside a
403
+ # block predicate's own `{ |x| ... }` body is not this expression's
404
+ # own addition. `parse` tries addition BEFORE `parse_block_opener`,
405
+ # so a paren-only depth count split
406
+ # kings.any? { |k| k.square.file == to.file + 1 && ... }
407
+ # at that inner `+`, turning the whole expression into a nonsense
408
+ # Addition whose left operand then walked "any? { |k| k" into an
409
+ # Array as an attribute path — "TypeError: no implicit conversion
410
+ # of Symbol into Integer", the same signature every unsupported
411
+ # construct raises, which is what let this hide. Found live in a
412
+ # downstream chess domain's castling given; the evaluator's own
413
+ # top_level_index has counted braces since its own version of this
414
+ # exact lesson.
415
+ def split_addition(expr)
416
+ depth = 0
417
+ quote = nil
418
+
419
+ expr.each_char.with_index do |char, index|
420
+ if quote
421
+ quote = nil if char == quote
422
+ elsif ['"', "'"].include?(char)
423
+ quote = char
424
+ # `[`/`]` -- the identical lesson this method's own `(`/`{`
425
+ # comment already names, a third time (found live via the
426
+ # type-directed bounded-exhaustive expression generator,
427
+ # Phase 7 of the equivalence-gap plan): `ArrayLiteral` can
428
+ # appear as a general sub-expression now, not only as
429
+ # `.include?`'s own haystack, so an array element containing
430
+ # its own top-level `+` (`[0, 0 + 0]`) used to read as THIS
431
+ # expression's own addition split point -- the whole
432
+ # receiver before `.all?`/`.any?`/etc. torn in half before
433
+ # `parse_block_opener` ever saw it as one atomic leaf.
434
+ elsif ["(", "{", "["].include?(char)
435
+ depth += 1
436
+ elsif [")", "}", "]"].include?(char)
437
+ depth -= 1
438
+ elsif char == "+" && depth.zero?
439
+ return [expr[0...index].strip, expr[(index + 1)..].strip]
440
+ end
441
+ end
442
+ nil
443
+ end
444
+
445
+ def add(left, right)
446
+ require_number(left, "addition") + require_number(right, "addition")
447
+ end
448
+
449
+ def quoted?(expr)
450
+ return false if expr.length < 2
451
+
452
+ (expr.start_with?('"') && expr.end_with?('"')) ||
453
+ (expr.start_with?("'") && expr.end_with?("'"))
454
+ end
455
+
456
+ # Declared the same way in Vocabulary::SizedType
457
+ # (language/bluebook/vocabulary.bluebook) — spec/vocabulary_conformance_spec
458
+ # holds this equal to the language. Shared by .size and .empty?,
459
+ # which admit the same set for the same reason.
460
+ SIZED_TYPES = Hecks::Vocabulary.fetch("SizedType")
461
+
462
+ def size_of(value)
463
+ return value.size if value.is_a?(Array) || value.is_a?(String) || value.is_a?(Hash)
464
+
465
+ raise EvaluationError, "size expects a list or string, got #{describe(value)}"
466
+ end
467
+
468
+ def emptiness_of(value)
469
+ return value.empty? if value.is_a?(Array) || value.is_a?(String) || value.is_a?(Hash)
470
+
471
+ raise EvaluationError, "empty? expects a list or string, got #{describe(value)}"
472
+ end
473
+
474
+ # `.split("SEP")` -- vendored addition, see the `Split` struct's
475
+ # own comment above. Only a String receiver makes sense to
476
+ # split -- unlike `.length`/`.size`/`.empty?`, which are already
477
+ # meaningful over Array/Hash too, `.split` is a String-only
478
+ # method in the corpus's own usage (every occurrence found this
479
+ # pass splits a Phrase's own string value).
480
+ def split_value(value, separator)
481
+ raise EvaluationError, "split expects a string, got #{describe(value)}" unless value.is_a?(String)
482
+
483
+ value.split(separator)
484
+ end
485
+
486
+ # `.last` -- vendored addition, see the `Last` struct's own
487
+ # comment above. Duck-typed on `respond_to?(:last)` rather than
488
+ # hard-coding Array -- the one corpus usage found this pass
489
+ # (`Query::Phrase`'s `.split("::").last`) always receives a
490
+ # `Split`-produced Array, but nothing about `.last` itself is
491
+ # Array-specific, and this matches `Empty`/`Size`'s own
492
+ # duck-typed-over-a-known-set precedent without inventing a
493
+ # narrower rule than the method needs.
494
+ def last_of(value)
495
+ return value.last if value.respond_to?(:last)
496
+
497
+ raise EvaluationError, "last expects a list, got #{describe(value)}"
498
+ end
499
+
500
+ # `.first` -- see the `First` struct's own comment above.
501
+ # `last_of` with the one method swapped, same duck-typed
502
+ # reasoning.
503
+ def first_of(value)
504
+ return value.first if value.respond_to?(:first)
505
+
506
+ raise EvaluationError, "first expects a list, got #{describe(value)}"
507
+ end
508
+
509
+ # `.start_with?("prefix")` -- vendored addition, see the
510
+ # `StartsWith` struct's own comment above. String-only, same
511
+ # reasoning as `.split` above -- every corpus usage found this
512
+ # pass (`Params`'s own JSON-object-shape invariant) receives a
513
+ # plain String field.
514
+ def starts_with?(value, substring)
515
+ raise EvaluationError, "start_with? expects a string, got #{describe(value)}" unless value.is_a?(String)
516
+
517
+ value.start_with?(substring)
518
+ end
519
+
520
+ # `.end_with?("suffix")` -- vendored addition, see the `EndsWith`
521
+ # struct's own comment above. Same String-only reasoning as
522
+ # `start_with?` immediately above.
523
+ def ends_with?(value, substring)
524
+ raise EvaluationError, "end_with? expects a string, got #{describe(value)}" unless value.is_a?(String)
525
+
526
+ value.end_with?(substring)
527
+ end
528
+
529
+ # Declared the same way in Vocabulary::ToStringType
530
+ # (language/bluebook/vocabulary.bluebook) — spec/vocabulary_conformance_spec
531
+ # holds this equal to the language.
532
+ TO_STRING_TYPES = Hecks::Vocabulary.fetch("ToStringType")
533
+
534
+ def string_of(value)
535
+ 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 ""
540
+ else
541
+ raise EvaluationError, "to_s expects a scalar, got #{describe(value)}"
542
+ end
543
+ end
544
+
545
+ def match_suffix(expr, suffixes)
546
+ suffixes.each do |suffix|
547
+ marker = ".#{suffix}"
548
+ return [expr[0...-marker.length], suffix] if expr.end_with?(marker)
549
+ end
550
+ nil
551
+ end
552
+
553
+ def apply_sign_test(node, value)
554
+ number = numeric(value)
555
+ raise EvaluationError, "#{node.test} expects a number, got #{describe(value)}" unless number
556
+
557
+ Evaluator.apply(node.operator, number, 0)
558
+ end
559
+
560
+ # FOUND LIVE via the type-directed bounded-exhaustive expression
561
+ # generator (Phase 7, equivalence-gap plan — spec/
562
+ # bounded_exhaustive_expression_spec.rb): `.modulo(`'s own
563
+ # argument position accepts any numeric sub-expression, including
564
+ # ANOTHER `.modulo(...)` call — `0.modulo(num_b.modulo(-1))` is
565
+ # perfectly well-typed — but `expr.rindex(marker)` finds the
566
+ # RIGHTMOST (innermost) `.modulo(` in the whole string, not the
567
+ # OUTERMOST one a nested call needs split at. For that expression
568
+ # it found the INNER `.modulo(` (inside `num_b.modulo(-1)`) and
569
+ # split there, producing a receiver of `"0.modulo(num_b"` and a
570
+ # divisor of `"-1)"` — both garbage, both re-parsed as bogus
571
+ # `Lookup` paths, both then refusing with "cannot resolve" — a
572
+ # SILENT MISPARSE that happened to fail safe into a real
573
+ # `EvaluationError` rather than a raw crash, which is exactly why
574
+ # this had gone unnoticed: nothing before this generator existed
575
+ # ever fed `.modulo` a nested `.modulo` call, random fuzzing
576
+ # essentially never manufactures that specific shape by chance,
577
+ # and the resulting refusal LOOKS like an ordinary, correct one
578
+ # unless you already know every name this generator's own
579
+ # synthetic state declares (real corpus authors would see this as
580
+ # a mysterious "cannot resolve" on text they never wrote).
581
+ #
582
+ # Fixed the same way `split_addition`/`Evaluator.top_level_index`
583
+ # already handle nested `(`/`{` elsewhere in this exact file:
584
+ # find the FIRST (leftmost, outermost) occurrence of the marker,
585
+ # then track paren/quote depth from there to find ITS OWN
586
+ # matching close — not just strip the string's own trailing `)`
587
+ # and hope it belongs to this call.
588
+ # Not just the FIRST occurrence, either — `.modulo` also CHAINS
589
+ # (`x.modulo(a).modulo(b)`, the receiver of the OUTER call itself
590
+ # ending in a `.modulo(...)` call), a second real shape the
591
+ # leftmost-occurrence-only version of this fix still mis-parsed:
592
+ # the first `.modulo(`'s own matching close paren lands mid-
593
+ # string (right after `a)`, before the second `.modulo(b)`), so
594
+ # it correctly fails the "reaches the end" check below and must
595
+ # be tried again at the NEXT occurrence rather than giving up.
596
+ # Trying occurrences strictly left to right and taking the FIRST
597
+ # one whose matching close reaches the string's last character
598
+ # handles both shapes with the same rule: for NESTING
599
+ # (`.modulo(x.modulo(y))`), the leftmost (outer) occurrence's own
600
+ # paren-depth tracking already walks straight through the inner
601
+ # call to the true final `)`; for CHAINING, the leftmost
602
+ # occurrence's close lands short and is rejected, so the next
603
+ # occurrence (the true outermost call) is tried instead.
604
+ def match_call(expr, marker)
605
+ start = 0
606
+ while (index = expr.index(marker, start))
607
+ close = matching_paren(expr, index + marker.length)
608
+ return [expr[0...index], expr[(index + marker.length)...close]] if close == expr.length - 1
609
+
610
+ start = index + 1
611
+ end
612
+ nil
613
+ end
614
+
615
+ # `matching_brace` (resolver/block_predicates.rb)'s own twin, one
616
+ # bracket pair over: `start` is the index just past the OPENING
617
+ # `(` already consumed by the caller (depth starts at 1, not 0,
618
+ # for the same reason).
619
+ def matching_paren(expr, start)
620
+ depth = 1
621
+ quote = nil
622
+ index = start
623
+ while index < expr.length
624
+ char = expr[index]
625
+ if quote
626
+ quote = nil if char == quote
627
+ elsif ['"', "'"].include?(char)
628
+ quote = char
629
+ elsif char == "("
630
+ depth += 1
631
+ elsif char == ")"
632
+ depth -= 1
633
+ return index if depth.zero?
634
+ end
635
+ index += 1
636
+ end
637
+ nil
638
+ end
639
+
640
+ # Both operands are coerced to a real Integer/Float BEFORE the
641
+ # zero-check, and the check reads the COERCED divisor — not the
642
+ # raw `divisor_value` (which might not even respond to `.zero?`,
643
+ # a String for instance) and not a `.to_i`-truncated stand-in for
644
+ # it either. The old order checked a truncated `divisor.to_i`
645
+ # AFTER already validating the untruncated value wasn't zero, so
646
+ # a divisor merely small (`0.3`, truncating to `0`) sailed past
647
+ # the guard and then blew up `Integer#%` with a raw
648
+ # `ZeroDivisionError` the moment it reached zero anyway.
649
+ #
650
+ # The modulo itself is plain `%` on the coerced values, matching
651
+ # `add`'s own no-truncation precedent just above — Ruby's native
652
+ # `%` already handles every Integer/Float combination correctly
653
+ # (promoting to Float when either side is one), so rounding both
654
+ # operands down to Integer first was pure data loss with no
655
+ # purpose: `7.5.modulo(2.5)` silently became `7 % 2` (`1`)
656
+ # instead of the real `0.0`.
657
+ def apply_modulo(receiver_value, divisor_value)
658
+ receiver = require_number(receiver_value, "modulo")
659
+ divisor = require_number(divisor_value, "modulo")
660
+ raise EvaluationError, "divided by 0" if divisor.zero?
661
+
662
+ receiver % divisor
663
+ end
664
+
665
+ def lookup(expr, state, attrs)
666
+ return unwrap_scalar(fetch(expr, state, attrs)) unless expr.include?(".")
667
+
668
+ head, *rest = expr.split(".")
669
+ unwrap_scalar(walk_path(fetch(head, state, attrs), rest))
670
+ end
671
+
672
+ # Walks a list of dotted segments through an already-resolved
673
+ # Hash-like value — extracted from `lookup`'s own reduce so
674
+ # `found_of` (the `Find` node's own path projection) can walk a
675
+ # `.find { ... }`-produced element the identical way `lookup`
676
+ # walks a plain attribute path, rather than duplicating the
677
+ # symbol-or-string key step twice in this file. `key?` decides
678
+ # which spelling answers — a bare `||` between the two would
679
+ # treat a genuinely-held `false` the same as an absent key and
680
+ # fall through to the other spelling, landing on `nil`.
681
+ def walk_path(value, segments)
682
+ segments.reduce(value) do |current, segment|
683
+ break nil unless current.respond_to?(:[])
684
+
685
+ if current.is_a?(Hash)
686
+ sym = segment.to_sym
687
+ current.key?(sym) ? current[sym] : current[segment]
688
+ else
689
+ begin
690
+ current[segment]
691
+ rescue TypeError
692
+ # M9 (docs/audits/2026-08-10-main-bug-audit.md): a dotted
693
+ # path can walk onto an Array (e.g. the result of `.split`,
694
+ # or a `list_of` attribute) — Array#[] demands an
695
+ # Integer/Range and raises a raw TypeError for a String
696
+ # segment ("no implicit conversion of String into
697
+ # Integer"), which used to cross straight past this
698
+ # sublanguage's own refusal boundary and crash the
699
+ # runtime instead of reading as "this predicate doesn't
700
+ # apply here."
701
+ raise EvaluationError,
702
+ "cannot read #{segment.inspect} from #{describe(current)}"
703
+ end
704
+ end
705
+ end
706
+ end
707
+
708
+ # `field == "literal"` -- vendored addition, not (yet) upstream
709
+ # hecks (migration plan task 8): the third-most pervasive
710
+ # dispatch-time gap this pass found, same family as `.match?`/
711
+ # `.present?` above -- a lookup of a single-field
712
+ # scalar-convenience value object (the exact shape `Value.
713
+ # from_identifier`/`Value::Coercion#fields_for`'s own single-
714
+ # field auto-unwrap already treats as "this VO IS its scalar"
715
+ # everywhere else in this runtime) came back as the `Value`
716
+ # wrapper itself, never unwrapped for READING -- so `Value#==`
717
+ # (which only ever equals another `Value` instance) silently
718
+ # refused every `guarantees "..." do status == "active" end` /
719
+ # `expects "..." do trash_day.present? end`-shaped bare
720
+ # comparison against a raw literal. Confirmed corpus-wide, not
721
+ # one file's mistake: bin-buddy alone has this exact `field ==
722
+ # "literal"` shape in plan.bluebook, service_task.bluebook,
723
+ # route.bluebook, and subscription.bluebook, all equally silent
724
+ # until a real dispatch (never validate) exercised the
725
+ # predicate. Scoped narrowly to the single-field `{value: X}`
726
+ # shape only.
727
+ #
728
+ # UPDATE 2026-08-18: originally scoped to unwrap ONLY the bare
729
+ # (undotted) case, on the belief that a dotted lookup only ever
730
+ # reaches into a VO's OWN field (`field.value`, `field.sub_
731
+ # field`) and so should keep walking `#[]` untouched. That
732
+ # belief held for the single-hop case but not for the general
733
+ # one: a dotted lookup that NAVIGATES THROUGH an entity/list
734
+ # element to a nested field (`leg.voyage`, where `voyage` is
735
+ # itself a single-field VO) landed on the very same unwrapped-
736
+ # `Value` shape the bare case fixed, and hit the identical
737
+ # silent `Value#==` failure -- comparing it against a raw
738
+ # literal or another unwrapped VO returned false for everything,
739
+ # no error. The terminal value of a dotted walk deserves the
740
+ # same "this VO IS its scalar" treatment as a bare lookup's
741
+ # result; only the INTERMEDIATE hops need raw `#[]` addressing
742
+ # to keep navigating. `unwrap_scalar` is idempotent on an
743
+ # already-raw scalar (a String/Integer doesn't respond to
744
+ # `#to_h`), so this is safe for the existing `field.value`-
745
+ # shaped dotted lookups too -- they already returned a raw
746
+ # scalar and are unaffected.
747
+ def unwrap_scalar(value)
748
+ return value unless value.respond_to?(:to_h) && !value.is_a?(Hash) && !value.is_a?(Array)
749
+
750
+ hash = value.to_h
751
+ hash.size == 1 && hash.key?(:value) ? hash[:value] : value
752
+ end
753
+
754
+ def fetch(name, state, attrs)
755
+ key = name.to_sym
756
+ return attrs[key] if attrs.key?(key)
757
+ return state[key] if known?(state, key)
758
+
759
+ raise EvaluationError, "cannot resolve #{name.inspect} — no such attribute or argument"
760
+ end
761
+
762
+ def known?(state, key)
763
+ return state.key?(key) if state.respond_to?(:key?)
764
+
765
+ !state[key].nil?
766
+ end
767
+
768
+ def numeric(value)
769
+ value if value.is_a?(Integer) || value.is_a?(Float)
770
+ end
771
+
772
+ def require_number(value, operation)
773
+ numeric(value) ||
774
+ raise(EvaluationError, "#{operation} expects a number, got #{describe(value)}")
775
+ end
776
+
777
+ def describe(value) = Rendering.describe(value)
778
+ end
779
+ end
780
+ end
781
+ end