hecks 0.2.0 → 0.3.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 (408) 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 +403 -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 +62 -0
  12. data/lib/hecks/adapters/driven/heki/journal.rb +51 -0
  13. data/lib/hecks/adapters/driven/heki/saga_store.rb +0 -0
  14. data/lib/hecks/adapters/driven/heki/snapshot.rb +38 -0
  15. data/lib/hecks/adapters/driven/heki.adapter +4 -0
  16. data/lib/hecks/adapters/driven/heki.rb +148 -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 +131 -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 +80 -0
  28. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +168 -0
  29. data/lib/hecks/adapters/driven/postgres.adapter +5 -0
  30. data/lib/hecks/adapters/driven/postgres.rb +391 -0
  31. data/lib/hecks/adapters/driven/postgres_era/lineage/era_store.rb +171 -0
  32. data/lib/hecks/adapters/driven/postgres_era/lineage/field_cache.rb +190 -0
  33. data/lib/hecks/adapters/driven/postgres_era/lineage/head_compiler.rb +429 -0
  34. data/lib/hecks/adapters/driven/postgres_era/lineage/mint_transaction.rb +166 -0
  35. data/lib/hecks/adapters/driven/postgres_era/lineage/provisioning.rb +286 -0
  36. data/lib/hecks/adapters/driven/postgres_era/lineage/resumable_backfill.rb +168 -0
  37. data/lib/hecks/adapters/driven/postgres_era/lineage/tail_merge.rb +163 -0
  38. data/lib/hecks/adapters/driven/postgres_era/lineage/transform_installer.rb +114 -0
  39. data/lib/hecks/adapters/driven/postgres_era/lineage.rb +137 -0
  40. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/coverage_check.rb +89 -0
  41. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/era_resolver.rb +82 -0
  42. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
  43. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/minter.rb +132 -0
  44. data/lib/hecks/adapters/driven/postgres_era/lineage_manager.rb +74 -0
  45. data/lib/hecks/adapters/driven/postgres_era.adapter +6 -0
  46. data/lib/hecks/adapters/driven/postgres_era.rb +671 -0
  47. data/lib/hecks/adapters/driven/prism.adapter +3 -0
  48. data/lib/hecks/adapters/driven/prism.rb +80 -0
  49. data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
  50. data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
  51. data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
  52. data/lib/hecks/adapters/driven/sqlite/codec.rb +63 -0
  53. data/lib/hecks/adapters/driven/sqlite/projection.rb +105 -0
  54. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +180 -0
  55. data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
  56. data/lib/hecks/adapters/driven/sqlite.rb +285 -0
  57. data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
  58. data/lib/hecks/adapters/driven/system_clock.rb +14 -0
  59. data/lib/hecks/adapters/driven.rb +36 -0
  60. data/lib/hecks/adapters.rb +6 -0
  61. data/lib/hecks/behaviors/dsl.rb +96 -0
  62. data/lib/hecks/behaviors/expectations.rb +280 -0
  63. data/lib/hecks/behaviors/ir.rb +31 -0
  64. data/lib/hecks/behaviors/rspec.rb +42 -0
  65. data/lib/hecks/behaviors/runner.rb +96 -0
  66. data/lib/hecks/behaviors.rb +25 -0
  67. data/lib/hecks/bluebook/aggregate.rb +108 -0
  68. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
  69. data/lib/hecks/bluebook/assembly/build.rb +48 -0
  70. data/lib/hecks/bluebook/assembly/contract.rb +112 -0
  71. data/lib/hecks/bluebook/assembly/contracts.rb +409 -0
  72. data/lib/hecks/bluebook/assembly/marks.rb +227 -0
  73. data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
  74. data/lib/hecks/bluebook/assembly.rb +78 -0
  75. data/lib/hecks/bluebook/attribute.rb +96 -0
  76. data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
  77. data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
  78. data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
  79. data/lib/hecks/bluebook/behaviour/command.rb +116 -0
  80. data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
  81. data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
  82. data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
  83. data/lib/hecks/bluebook/behaviour/lifecycle.rb +51 -0
  84. data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
  85. data/lib/hecks/bluebook/behaviour/process_manager.rb +38 -0
  86. data/lib/hecks/bluebook/behaviour/query.rb +10 -0
  87. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
  88. data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
  89. data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
  90. data/lib/hecks/bluebook/chapter.rb +68 -0
  91. data/lib/hecks/bluebook/command.rb +124 -0
  92. data/lib/hecks/bluebook/domain_port.rb +102 -0
  93. data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
  94. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +906 -0
  95. data/lib/hecks/bluebook/dsl/attribute_collector.rb +327 -0
  96. data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
  97. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1018 -0
  98. data/lib/hecks/bluebook/dsl/command_builder.rb +626 -0
  99. data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
  100. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
  101. data/lib/hecks/bluebook/dsl/entity_builder.rb +305 -0
  102. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
  103. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
  104. data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
  105. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
  106. data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
  107. data/lib/hecks/bluebook/dsl/policy_builder.rb +120 -0
  108. data/lib/hecks/bluebook/dsl/port_builder.rb +30 -0
  109. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
  110. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +207 -0
  111. data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
  112. data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
  113. data/lib/hecks/bluebook/dsl/rule_reference.rb +172 -0
  114. data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
  115. data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
  116. data/lib/hecks/bluebook/dsl/word_gate.rb +221 -0
  117. data/lib/hecks/bluebook/dsl/world_builder.rb +77 -0
  118. data/lib/hecks/bluebook/dsl.rb +45 -0
  119. data/lib/hecks/bluebook/entity.rb +103 -0
  120. data/lib/hecks/bluebook/expression/canonical_form.rb +55 -0
  121. data/lib/hecks/bluebook/expression/evaluator.rb +261 -0
  122. data/lib/hecks/bluebook/expression/projection.json +170 -0
  123. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
  124. data/lib/hecks/bluebook/expression/resolver.rb +630 -0
  125. data/lib/hecks/bluebook/expression.rb +13 -0
  126. data/lib/hecks/bluebook/hexagon.rb +60 -0
  127. data/lib/hecks/bluebook/lifecycle.rb +42 -0
  128. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
  129. data/lib/hecks/bluebook/meta_validator/judge.rb +559 -0
  130. data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
  131. data/lib/hecks/bluebook/meta_validator/port_judge.rb +47 -0
  132. data/lib/hecks/bluebook/meta_validator/readings.rb +350 -0
  133. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +312 -0
  134. data/lib/hecks/bluebook/meta_validator/shapes.rb +246 -0
  135. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
  136. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
  137. data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
  138. data/lib/hecks/bluebook/meta_validator.rb +452 -0
  139. data/lib/hecks/bluebook/model_check.rb +318 -0
  140. data/lib/hecks/bluebook/pattern_subset.rb +120 -0
  141. data/lib/hecks/bluebook/policy.rb +41 -0
  142. data/lib/hecks/bluebook/process_manager.rb +86 -0
  143. data/lib/hecks/bluebook/project_discovery.rb +30 -0
  144. data/lib/hecks/bluebook/project_loader.rb +40 -0
  145. data/lib/hecks/bluebook/project_register.rb +82 -0
  146. data/lib/hecks/bluebook/query.rb +61 -0
  147. data/lib/hecks/bluebook/read_model.rb +109 -0
  148. data/lib/hecks/bluebook/reference.rb +74 -0
  149. data/lib/hecks/bluebook/smoke_test.rb +166 -0
  150. data/lib/hecks/bluebook/synthesizer.rb +95 -0
  151. data/lib/hecks/bluebook/translation.rb +92 -0
  152. data/lib/hecks/bluebook/value_object.rb +58 -0
  153. data/lib/hecks/bluebook.rb +74 -0
  154. data/lib/hecks/codemod.rb +342 -0
  155. data/lib/hecks/construct.rb +71 -0
  156. data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
  157. data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
  158. data/lib/hecks/deploy/oidc.json +18 -0
  159. data/lib/hecks/doc/reference.rb +389 -0
  160. data/lib/hecks/embryonaut_bluebook.rb +75 -0
  161. data/lib/hecks/facade/cli_door.rb +116 -0
  162. data/lib/hecks/facade/cli_runner.rb +190 -0
  163. data/lib/hecks/facade/command_request.rb +105 -0
  164. data/lib/hecks/facade/handle.rb +173 -0
  165. data/lib/hecks/facade/json_door.rb +154 -0
  166. data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
  167. data/lib/hecks/facade/surface/chapter.rb +107 -0
  168. data/lib/hecks/facade/surface.rb +48 -0
  169. data/lib/hecks/facade.rb +44 -0
  170. data/lib/hecks/forms/app.rb +300 -0
  171. data/lib/hecks/forms/command_form_renderer.rb +113 -0
  172. data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
  173. data/lib/hecks/forms/field_renderer.rb +170 -0
  174. data/lib/hecks/forms/field_shape.rb +232 -0
  175. data/lib/hecks/forms/html.rb +53 -0
  176. data/lib/hecks/forms/index_renderer.rb +35 -0
  177. data/lib/hecks/forms/page.rb +157 -0
  178. data/lib/hecks/forms/params.rb +132 -0
  179. data/lib/hecks/forms/query_form_renderer.rb +114 -0
  180. data/lib/hecks/forms/record_renderer.rb +115 -0
  181. data/lib/hecks/forms/record_table.rb +63 -0
  182. data/lib/hecks/forms/reference_options.rb +30 -0
  183. data/lib/hecks/forms/value_object_shape.rb +46 -0
  184. data/lib/hecks/forms.rb +54 -0
  185. data/lib/hecks/fqn.rb +94 -0
  186. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
  187. data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
  188. data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
  189. data/lib/hecks/framework/bluebook/governance.bluebook +130 -0
  190. data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
  191. data/lib/hecks/framework/oidc.json +39 -0
  192. data/lib/hecks/framework.rb +90 -0
  193. data/lib/hecks/freezer.rb +67 -0
  194. data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
  195. data/lib/hecks/fuzzing/isolated_boot.rb +92 -0
  196. data/lib/hecks/fuzzing/properties.rb +1146 -0
  197. data/lib/hecks/fuzzing/replay.rb +635 -0
  198. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
  199. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +54 -0
  200. data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
  201. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +185 -0
  202. data/lib/hecks/fuzzing/sequence_generator.rb +130 -0
  203. data/lib/hecks/fuzzing/value_generator.rb +177 -0
  204. data/lib/hecks/fuzzing.rb +12 -0
  205. data/lib/hecks/grammar/evolve.rb +291 -0
  206. data/lib/hecks/grammar/expression.bluebook +417 -0
  207. data/lib/hecks/grammar/expression_operators.json +1152 -0
  208. data/lib/hecks/grammar/grammar.hecksagon +20 -0
  209. data/lib/hecks/grammar/oidc.json +69 -0
  210. data/lib/hecks/grammar/translation.bluebook +203 -0
  211. data/lib/hecks/grammar.rb +178 -0
  212. data/lib/hecks/ir.rb +126 -0
  213. data/lib/hecks/language/adapter.bluebook +116 -0
  214. data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
  215. data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
  216. data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
  217. data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
  218. data/lib/hecks/language/bluebook/command.bluebook +437 -0
  219. data/lib/hecks/language/bluebook/entity.bluebook +316 -0
  220. data/lib/hecks/language/bluebook/policy.bluebook +172 -0
  221. data/lib/hecks/language/bluebook/process_manager.bluebook +264 -0
  222. data/lib/hecks/language/bluebook/projection.bluebook +267 -0
  223. data/lib/hecks/language/bluebook/query.bluebook +245 -0
  224. data/lib/hecks/language/bluebook/shape.bluebook +292 -0
  225. data/lib/hecks/language/bluebook/syntax.bluebook +447 -0
  226. data/lib/hecks/language/bluebook/vocabulary.bluebook +364 -0
  227. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
  228. data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
  229. data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
  230. data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
  231. data/lib/hecks/language/oidc.json +313 -0
  232. data/lib/hecks/language/port.bluebook +92 -0
  233. data/lib/hecks/language/translation/translation.bluebook +110 -0
  234. data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
  235. data/lib/hecks/language/world/wiring.bluebook +62 -0
  236. data/lib/hecks/language/world/world.bluebook +84 -0
  237. data/lib/hecks/literal.rb +125 -0
  238. data/lib/hecks/naming.rb +121 -0
  239. data/lib/hecks/ports/access_control.port +4 -0
  240. data/lib/hecks/ports/access_control.rb +62 -0
  241. data/lib/hecks/ports/agent/answers.rb +104 -0
  242. data/lib/hecks/ports/agent.port +4 -0
  243. data/lib/hecks/ports/agent.rb +167 -0
  244. data/lib/hecks/ports/authentication.port +4 -0
  245. data/lib/hecks/ports/authentication.rb +50 -0
  246. data/lib/hecks/ports/authorization.port +4 -0
  247. data/lib/hecks/ports/authorization.rb +59 -0
  248. data/lib/hecks/ports/clock.port +4 -0
  249. data/lib/hecks/ports/clock.rb +62 -0
  250. data/lib/hecks/ports/extraction.port +4 -0
  251. data/lib/hecks/ports/extraction.rb +37 -0
  252. data/lib/hecks/ports/identity_assignment.port +4 -0
  253. data/lib/hecks/ports/identity_assignment.rb +45 -0
  254. data/lib/hecks/ports/identity_generation.port +4 -0
  255. data/lib/hecks/ports/identity_generation.rb +49 -0
  256. data/lib/hecks/ports/identity_resolution.port +4 -0
  257. data/lib/hecks/ports/identity_resolution.rb +40 -0
  258. data/lib/hecks/ports/loading.port +4 -0
  259. data/lib/hecks/ports/loading.rb +13 -0
  260. data/lib/hecks/ports/persistence/append_only.rb +102 -0
  261. data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
  262. data/lib/hecks/ports/persistence/execution.rb +18 -0
  263. data/lib/hecks/ports/persistence/lineage.rb +277 -0
  264. data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
  265. data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
  266. data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
  267. data/lib/hecks/ports/persistence.port +4 -0
  268. data/lib/hecks/ports/persistence.rb +39 -0
  269. data/lib/hecks/ports/projection.port +4 -0
  270. data/lib/hecks/ports/projection.rb +76 -0
  271. data/lib/hecks/ports/query/in_memory.rb +60 -0
  272. data/lib/hecks/ports/query/ordering.rb +41 -0
  273. data/lib/hecks/ports/query.rb +36 -0
  274. data/lib/hecks/ports.rb +27 -0
  275. data/lib/hecks/projections/diagrams.rb +413 -0
  276. data/lib/hecks/projections/ir.rb +18 -0
  277. data/lib/hecks/projections/model/deviations.rb +98 -0
  278. data/lib/hecks/projections/model.rb +145 -0
  279. data/lib/hecks/projections/oidc.rb +110 -0
  280. data/lib/hecks/projections/parser_table.rb +159 -0
  281. data/lib/hecks/projections/reference.rb +38 -0
  282. data/lib/hecks/projections/shape.rb +31 -0
  283. data/lib/hecks/projections/statements.rb +110 -0
  284. data/lib/hecks/projections/vocabulary.rb +100 -0
  285. data/lib/hecks/projections.rb +32 -0
  286. data/lib/hecks/projector/cli_projector.rb +437 -0
  287. data/lib/hecks/projector/docs_projector.rb +321 -0
  288. data/lib/hecks/projector/exporter.rb +141 -0
  289. data/lib/hecks/projector/ir_projector.rb +18 -0
  290. data/lib/hecks/projector/narrate_projector.rb +243 -0
  291. data/lib/hecks/projector/target.rb +97 -0
  292. data/lib/hecks/projector.rb +186 -0
  293. data/lib/hecks/query_ir.rb +392 -0
  294. data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
  295. data/lib/hecks/query_specification/common/comparators.rb +25 -0
  296. data/lib/hecks/query_specification/common/comparison.rb +174 -0
  297. data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
  298. data/lib/hecks/query_specification/common/dsl.rb +58 -0
  299. data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
  300. data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
  301. data/lib/hecks/query_specification/common/null_policy.rb +80 -0
  302. data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
  303. data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
  304. data/lib/hecks/query_specification/common/options.rb +32 -0
  305. data/lib/hecks/query_specification/common/order_by.rb +9 -0
  306. data/lib/hecks/query_specification/common/specification.rb +10 -0
  307. data/lib/hecks/query_specification/common/where_clause.rb +9 -0
  308. data/lib/hecks/query_specification/field_path.rb +89 -0
  309. data/lib/hecks/query_specification/hop_path.rb +130 -0
  310. data/lib/hecks/query_specification/read_model/specification.rb +18 -0
  311. data/lib/hecks/query_specification.rb +14 -0
  312. data/lib/hecks/rendering.rb +48 -0
  313. data/lib/hecks/router/namespace_installer.rb +157 -0
  314. data/lib/hecks/router.rb +70 -0
  315. data/lib/hecks/runtime/caller.rb +50 -0
  316. data/lib/hecks/runtime/capability_graph.rb +44 -0
  317. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +124 -0
  318. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +248 -0
  319. data/lib/hecks/runtime/command_interpreter.rb +413 -0
  320. data/lib/hecks/runtime/command_rules/admissibility.rb +307 -0
  321. data/lib/hecks/runtime/command_rules/arithmetic.rb +254 -0
  322. data/lib/hecks/runtime/command_rules/authorization.rb +64 -0
  323. data/lib/hecks/runtime/command_rules/emission.rb +34 -0
  324. data/lib/hecks/runtime/command_rules/references.rb +181 -0
  325. data/lib/hecks/runtime/command_rules.rb +28 -0
  326. data/lib/hecks/runtime/dependency_planning.rb +231 -0
  327. data/lib/hecks/runtime/dispatcher.rb +286 -0
  328. data/lib/hecks/runtime/entity_element.rb +253 -0
  329. data/lib/hecks/runtime/entity_interpreter.rb +228 -0
  330. data/lib/hecks/runtime/era_check.rb +136 -0
  331. data/lib/hecks/runtime/era_guard/shape_diff.rb +128 -0
  332. data/lib/hecks/runtime/era_guard.rb +193 -0
  333. data/lib/hecks/runtime/era_tamper.rb +61 -0
  334. data/lib/hecks/runtime/errors.rb +100 -0
  335. data/lib/hecks/runtime/event.rb +51 -0
  336. data/lib/hecks/runtime/identity.rb +128 -0
  337. data/lib/hecks/runtime/instance.rb +120 -0
  338. data/lib/hecks/runtime/interpreting.rb +69 -0
  339. data/lib/hecks/runtime/loader.rb +143 -0
  340. data/lib/hecks/runtime/policy_interpreter.rb +366 -0
  341. data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
  342. data/lib/hecks/runtime/query_interpreter.rb +254 -0
  343. data/lib/hecks/runtime/reaction_invocation.rb +245 -0
  344. data/lib/hecks/runtime/read_model_interpreter.rb +266 -0
  345. data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
  346. data/lib/hecks/runtime/reference_hop.rb +99 -0
  347. data/lib/hecks/runtime/refusal_wording.rb +124 -0
  348. data/lib/hecks/runtime/registry/saga_persistence.rb +70 -0
  349. data/lib/hecks/runtime/registry/verification.rb +158 -0
  350. data/lib/hecks/runtime/registry.rb +218 -0
  351. data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
  352. data/lib/hecks/runtime/routing.rb +96 -0
  353. data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
  354. data/lib/hecks/runtime/saga_interpreter.rb +301 -0
  355. data/lib/hecks/runtime/storage_shape.rb +120 -0
  356. data/lib/hecks/runtime/tenant_check.rb +84 -0
  357. data/lib/hecks/runtime/tenant_scope.rb +55 -0
  358. data/lib/hecks/runtime/value/admission.rb +112 -0
  359. data/lib/hecks/runtime/value/coercion.rb +500 -0
  360. data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
  361. data/lib/hecks/runtime/value.rb +125 -0
  362. data/lib/hecks/runtime.rb +95 -0
  363. data/lib/hecks/translation/audit/approval_digest.rb +31 -0
  364. data/lib/hecks/translation/audit/layer_one.rb +37 -0
  365. data/lib/hecks/translation/audit/layer_two.rb +77 -0
  366. data/lib/hecks/translation/audit/unfed_report.rb +42 -0
  367. data/lib/hecks/translation/audit.rb +70 -0
  368. data/lib/hecks/translation/reattest.rb +72 -0
  369. data/lib/hecks/translation/rule_compiler.rb +120 -0
  370. data/lib/hecks/translation/scaffold/differ.rb +183 -0
  371. data/lib/hecks/translation/scaffold/renderer.rb +41 -0
  372. data/lib/hecks/translation/scaffold/writer.rb +28 -0
  373. data/lib/hecks/translation/scaffold.rb +30 -0
  374. data/lib/hecks/translation.rb +11 -0
  375. data/lib/hecks/version.rb +16 -0
  376. data/lib/hecks/vocabulary.rb +213 -0
  377. data/lib/hecks.rb +118 -6
  378. metadata +401 -222
  379. data/bin/hecks +0 -7
  380. data/bin/hecks-package +0 -65
  381. data/bin/hecks_console +0 -12
  382. data/bin/hecks_serverless +0 -6
  383. data/lib/cli/build.rb +0 -14
  384. data/lib/cli/command_runner.rb +0 -28
  385. data/lib/cli/console.rb +0 -10
  386. data/lib/cli/generate.rb +0 -37
  387. data/lib/cli/hecks-cli.rb +0 -27
  388. data/lib/cli/test.rb +0 -57
  389. data/lib/console/commands.rb +0 -8
  390. data/lib/console/hecks-console.rb +0 -1
  391. data/lib/packager/README.md +0 -0
  392. data/lib/packager/app_runner.rb +0 -21
  393. data/lib/packager/args.rb +0 -26
  394. data/lib/packager/compatibility/fixnum.rb +0 -6
  395. data/lib/packager/hecks.rb +0 -39
  396. data/lib/packager/query_runner.rb +0 -21
  397. data/lib/packager/resources/Dockerfile +0 -11
  398. data/lib/packager/resources/app_binary +0 -7
  399. data/lib/packager/resources/bundle_config +0 -3
  400. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
  401. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
  402. data/lib/serverless/Domain +0 -32
  403. data/lib/serverless/cli.rb +0 -75
  404. data/lib/serverless/resources/command_name.js +0 -5
  405. data/lib/serverless/resources/environment.js +0 -7
  406. data/lib/serverless/resources/handler.js.tt +0 -28
  407. data/lib/serverless/resources/run_binary.js +0 -22
  408. data/lib/serverless/resources/serverless.yml +0 -20
@@ -0,0 +1,630 @@
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
+ # `receiver.match?(/pattern/)` -- vendored addition, not (yet)
62
+ # upstream hecks (migration plan task 8): confirmed the
63
+ # SINGLE most impactful corpus-wide dispatch-time gap of the
64
+ # whole migration -- `.match?(regex)` appears in nearly every
65
+ # value_object's format-validation rule across every corpus
66
+ # this migration touched (email/phone/ISO-8601-timestamp/zip
67
+ # patterns, dozens of files), and had NO parse support at all:
68
+ # it fell all the way through to the `Lookup` catch-all below,
69
+ # which tried to split the ENTIRE ".match?(/\A\d{5}\z/)" text
70
+ # on "." as if it were a dotted attribute path, and crashed with
71
+ # an opaque "no implicit conversion of Symbol into Integer"
72
+ # somewhere downstream -- confirmed live via a real dispatch,
73
+ # not validate (validate never evaluates a predicate body).
74
+ # `receiver` still needs its own parse (it may itself be a
75
+ # dotted lookup, e.g. `some_field.match?(...)`), the pattern
76
+ # text is taken as-is between the slashes (a Ruby Regexp literal,
77
+ # not the evaluator's own mini-grammar -- there is nothing to
78
+ # recurse into).
79
+ MatchesRegex = Struct.new(:receiver, :pattern, :flags, keyword_init: true)
80
+
81
+ # `.present?`/`.blank?` -- vendored addition, not (yet) upstream
82
+ # hecks (migration plan task 8): the second-most pervasive
83
+ # gap this pass found, right behind `.match?` -- confirmed real
84
+ # via the same "no such attribute" Lookup-fallback crash, since
85
+ # neither suffix existed in this leaf grammar at all. Rails-
86
+ # standard semantics (blank = nil, or responds_to?(:empty?) &&
87
+ # empty? ; present = !blank), not just `!nil?` -- matches what
88
+ # every corpus author actually means by `.present?` on a string
89
+ # field that could legitimately be "" rather than absent.
90
+ Presence = Struct.new(:receiver, :negated, keyword_init: true)
91
+
92
+ # `receiver.split("SEP")` -- vendored addition, not (yet) upstream
93
+ # hecks (migration plan task 9): confirmed the single
94
+ # highest-value new finding of the real-dispatch smoke sweep --
95
+ # the bus's own core `Phrase` value object (dispatch/lexicon/
96
+ # query/command_bus.bluebook, all four storehouse-kernel files,
97
+ # byte-identical text) validates its four-segment shape with
98
+ # `value.split("::").length == 4 && value.split("::").all? { |s|
99
+ # s.length > 0 }` -- `.split(` matched none of this grammar's
100
+ # known suffixes, so it fell through to the `Lookup` catch-all,
101
+ # which split the RAW EXPRESSION TEXT on "." (not the runtime
102
+ # value) and crashed with `TypeError: no implicit conversion of
103
+ # Symbol into Integer` the moment `String#[]` was handed a
104
+ # Symbol segment -- meaning every command taking a Phrase was
105
+ # entirely undispatchable, confirmed live via `Lexicon::Lexicon.
106
+ # Lookup`/`Query::Query.Run`, not inferred. Produces a real
107
+ # Array, deliberately not a scalar -- `.length`/`.all?` compose
108
+ # with it below the same way they compose with any other
109
+ # receiver, because `parse` recurses on the receiver text and
110
+ # the existing `Size` node (`.length`/`.size`) already treats
111
+ # "whatever `interpret` returns" as its receiver's value, not a
112
+ # fixed type. Separator is taken literally between the quotes,
113
+ # same "no sub-grammar to recurse into" precedent `MatchesRegex`
114
+ # already set for its own `/pattern/` text above.
115
+ Split = Struct.new(:receiver, :separator, keyword_init: true)
116
+
117
+ # `receiver.last` -- vendored addition, not (yet) upstream
118
+ # hecks (migration plan task 9), same pass as `Split` above
119
+ # and built for the same `Phrase`/`Path` value objects --
120
+ # `Query::Phrase`'s own invariant chains `.split("::").last.
121
+ # match?(...)` to check the fourth segment's casing. Same shape
122
+ # as `.length`/`.size` (Size) and `.empty?` (Empty) -- a plain
123
+ # receiver-in, scalar-out accessor, no sub-grammar of its own.
124
+ Last = Struct.new(:receiver, keyword_init: true)
125
+
126
+ # `receiver.first` -- sibling addition to `Last` immediately
127
+ # above, same shape, same vendored-not-upstream status. Added
128
+ # for the shipping domain's `legs.first` (an itinerary's
129
+ # departure leg), the exact mirror of `legs.last` (its arrival
130
+ # leg) that domain already leaned on -- `Last`'s own duck-typed-
131
+ # on-`respond_to?` reasoning applies unchanged, so `first_of`
132
+ # below is `last_of` with the one method swapped, not a new
133
+ # design.
134
+ First = Struct.new(:receiver, keyword_init: true)
135
+
136
+ # `receiver.start_with?("prefix")` / `receiver.end_with?("suffix")`
137
+ # -- vendored addition, not (yet) upstream hecks (migration
138
+ # plan task 9), the sibling gap the `Split`/`Last`/`BlockPredicate`
139
+ # pass above flagged and deliberately left unfixed --
140
+ # `Query::Query.Run`/`Dispatch::Dispatch.Route`/`CommandBus::
141
+ # CommandBus.Route`'s shared `Params` value object validates its
142
+ # JSON-object shape with `value.start_with?("{") && value.
143
+ # end_with?("}")` (dispatch/query/command_bus.bluebook, all three
144
+ # storehouse-kernel files, byte-identical text) -- `.start_with?(`/
145
+ # `.end_with?(` matched none of this grammar's known suffixes, so
146
+ # both fell through to the `Lookup` catch-all and crashed with the
147
+ # identical `TypeError: no implicit conversion of Symbol into
148
+ # Integer` shape `.split`/`.all?` used to, confirmed live via a
149
+ # real dispatch (not validate), not inferred. Two separate node
150
+ # types rather than one `mode:`-keyed struct (the `BlockPredicate`/
151
+ # `SignTest` precedent) -- `start_with?`/`end_with?` aren't two
152
+ # spellings of the same test the way `all?`/`any?`/`none?` are (one
153
+ # Array-aggregation family) or `positive?`/`negative?`/`zero?` are
154
+ # (one comparison-against-0 family) ; they check different ends of
155
+ # the same string and share nothing but their
156
+ # receiver-plus-literal-argument shape. `substring` is taken
157
+ # literally between the quotes, same "no sub-grammar to recurse
158
+ # into" precedent `Split`'s `separator`/`MatchesRegex`'s `pattern`
159
+ # already set.
160
+ StartsWith = Struct.new(:receiver, :substring, keyword_init: true)
161
+ EndsWith = Struct.new(:receiver, :substring, keyword_init: true)
162
+
163
+ module_function
164
+
165
+ def resolve(expr, state, attrs)
166
+ interpret(parse(expr), state, attrs)
167
+ end
168
+
169
+ def parse(expr)
170
+ expr = expr.to_s.strip
171
+
172
+ return Size.new(receiver: parse(Regexp.last_match(1))) if expr =~ /\A(.+)\.length\z/
173
+
174
+ return IntegerLiteral.new(value: Integer(expr, 10)) if expr.match?(/\A-?\d+\z/)
175
+ return FloatLiteral.new(value: Float(expr)) if expr.match?(/\A-?\d*\.\d+\z/)
176
+ return StringLiteral.new(value: expr[1..-2]) if quoted?(expr)
177
+ return BoolLiteral.new(value: true) if expr == "true"
178
+ return BoolLiteral.new(value: false) if expr == "false"
179
+ return NilLiteral.new if expr == "nil"
180
+
181
+ elements = array_elements(expr)
182
+ return ArrayLiteral.new(elements: elements.map { |element| parse(element) }) if elements
183
+
184
+ arithmetic = split_addition(expr)
185
+ return Addition.new(left: parse(arithmetic[0]), right: parse(arithmetic[1])) if arithmetic
186
+
187
+ sign = match_suffix(expr, SIGN_TESTS)
188
+ return sign_test_node(sign) if sign
189
+
190
+ return Empty.new(receiver: parse(Regexp.last_match(1))) if expr =~ /\A(.+)\.empty\?\z/
191
+ return ToS.new(receiver: parse(Regexp.last_match(1))) if expr =~ /\A(.+)\.to_s\z/
192
+
193
+ modulo = match_call(expr, ".modulo(")
194
+ return Modulo.new(receiver: parse(modulo[0]), divisor: parse(modulo[1])) if modulo
195
+
196
+ return Size.new(receiver: parse(Regexp.last_match(1))) if expr =~ /\A(.+)\.size\z/
197
+
198
+ if expr =~ /\A(.+)\.match\?\(\/(.*)\/([a-z]*)\)\z/m
199
+ return MatchesRegex.new(receiver: parse(Regexp.last_match(1)),
200
+ pattern: Regexp.last_match(2),
201
+ flags: Regexp.last_match(3))
202
+ end
203
+
204
+ return Presence.new(receiver: parse(Regexp.last_match(1)), negated: false) if expr =~ /\A(.+)\.present\?\z/
205
+ return Presence.new(receiver: parse(Regexp.last_match(1)), negated: true) if expr =~ /\A(.+)\.blank\?\z/
206
+
207
+ if expr =~ /\A(.+)\.split\("([^"]*)"\)\z/
208
+ return Split.new(receiver: parse(Regexp.last_match(1)), separator: Regexp.last_match(2))
209
+ end
210
+
211
+ return First.new(receiver: parse(Regexp.last_match(1))) if expr =~ /\A(.+)\.first\z/
212
+ return Last.new(receiver: parse(Regexp.last_match(1))) if expr =~ /\A(.+)\.last\z/
213
+
214
+ if expr =~ /\A(.+)\.start_with\?\("([^"]*)"\)\z/
215
+ return StartsWith.new(receiver: parse(Regexp.last_match(1)), substring: Regexp.last_match(2))
216
+ end
217
+
218
+ if expr =~ /\A(.+)\.end_with\?\("([^"]*)"\)\z/
219
+ return EndsWith.new(receiver: parse(Regexp.last_match(1)), substring: Regexp.last_match(2))
220
+ end
221
+
222
+ block_opener = parse_block_opener(expr)
223
+ return block_opener if block_opener
224
+
225
+ Lookup.new(path: expr)
226
+ end
227
+
228
+ def sign_test_node(parts)
229
+ receiver, test = parts
230
+ symbol = SIGN_TEST_OPERATORS.fetch(test)
231
+ operator = Evaluator::OPERATORS.find { |candidate| candidate.symbol == symbol }
232
+ SignTest.new(operator: operator, test: test, receiver: parse(receiver))
233
+ end
234
+
235
+ def interpret(node, state, attrs)
236
+ case node
237
+ when IntegerLiteral, FloatLiteral, StringLiteral, BoolLiteral then node.value
238
+ when ArrayLiteral then node.elements.map { |element| interpret(element, state, attrs) }
239
+ when NilLiteral then nil
240
+ when Addition
241
+ add(interpret(node.left, state, attrs), interpret(node.right, state, attrs))
242
+ when SignTest
243
+ apply_sign_test(node, interpret(node.receiver, state, attrs))
244
+ when Empty
245
+ emptiness_of(interpret(node.receiver, state, attrs))
246
+ when ToS
247
+ string_of(interpret(node.receiver, state, attrs))
248
+ when Modulo
249
+ apply_modulo(interpret(node.receiver, state, attrs), interpret(node.divisor, state, attrs))
250
+ when Size
251
+ size_of(interpret(node.receiver, state, attrs))
252
+ when MatchesRegex
253
+ matches_regex?(interpret(node.receiver, state, attrs), node.pattern, node.flags)
254
+ when Presence
255
+ present = !blank?(interpret(node.receiver, state, attrs))
256
+ node.negated ? !present : present
257
+ when Split
258
+ split_value(interpret(node.receiver, state, attrs), node.separator)
259
+ when Last
260
+ last_of(interpret(node.receiver, state, attrs))
261
+ when First
262
+ first_of(interpret(node.receiver, state, attrs))
263
+ when Find
264
+ found_of(node, interpret(node.receiver, state, attrs), state, attrs)
265
+ when StartsWith
266
+ starts_with?(interpret(node.receiver, state, attrs), node.substring)
267
+ when EndsWith
268
+ ends_with?(interpret(node.receiver, state, attrs), node.substring)
269
+ when BlockPredicate
270
+ evaluate_block_predicate(node, interpret(node.receiver, state, attrs), state, attrs)
271
+ when Lookup
272
+ lookup(node.path, state, attrs)
273
+ else
274
+ # Every leaf node `parse` can produce has a `when` above —
275
+ # a backstop against the day this grammar grows a new leaf
276
+ # type (this file's own history: MatchesRegex/Presence/
277
+ # Split/Last/First/Find/StartsWith/EndsWith/BlockPredicate
278
+ # were each added exactly this way, and each one — before it had an
279
+ # `interpret` arm — fell all the way through to the
280
+ # `Lookup` catch-all in `parse` and crashed downstream with
281
+ # an opaque type error, never here). A missing arm here
282
+ # would instead return bare `nil` silently, the one wrong-
283
+ # answer shape this leaf grammar has otherwise never
284
+ # allowed.
285
+ raise EvaluationError, "no interpreter handles #{node.class} — add a case before parse can produce it"
286
+ end
287
+ end
288
+
289
+ # `.present?`/`.blank?` -- vendored addition, see the
290
+ # `Presence` struct's own comment above. `nil` and `false` are
291
+ # blank ; a String/Array/Hash is blank when EMPTY, not merely
292
+ # falsy -- a VO-wrapped field that IS assigned (`{value: "x"}`,
293
+ # `Value#to_h`'d first) is present regardless of what its own
294
+ # inner value holds, matching how every VO-typed field in this
295
+ # corpus is actually shaped once set at all.
296
+ def blank?(value)
297
+ return true if value.nil? || value == false
298
+
299
+ # Duck-typed, not `value.is_a?(Runtime::Value)` -- this module
300
+ # is `Bluebook::Expression`, a different namespace tree from
301
+ # `Runtime::Value` entirely, and reaching across for a single
302
+ # class check is the exact cross-module coupling that already
303
+ # broke `Modulo`'s own `match_call` reference elsewhere in
304
+ # this file (found live while building this fix, not assumed).
305
+ value = value.to_h if value.respond_to?(:to_h) && !value.is_a?(Hash)
306
+ case value
307
+ when String, Array, Hash then value.empty?
308
+ else false
309
+ end
310
+ end
311
+
312
+ # `receiver.match?(/pattern/)` -- vendored addition, see the
313
+ # `MatchesRegex` struct's own comment above. `receiver_value` is
314
+ # coerced to a plain String first -- inlined here rather than
315
+ # calling `Evaluator#string_of` (a DIFFERENT module_function
316
+ # module; not actually in scope from inside Resolver despite
317
+ # `Modulo`'s own parse rule above calling a same-named
318
+ # `match_call` that has the identical cross-module problem --
319
+ # found live while building this, not assumed).
320
+ def matches_regex?(receiver_value, pattern, flags)
321
+ text = case receiver_value
322
+ when String, Symbol then receiver_value.to_s
323
+ when Integer, Float then receiver_value.to_s
324
+ when NilClass then ""
325
+ else
326
+ raise EvaluationError, "match? expects a scalar, got #{receiver_value.class}"
327
+ end
328
+
329
+ options = 0
330
+ options |= Regexp::IGNORECASE if flags.include?("i")
331
+ options |= Regexp::MULTILINE if flags.include?("m")
332
+ options |= Regexp::EXTENDED if flags.include?("x")
333
+
334
+ Regexp.new(pattern, options).match?(text)
335
+ end
336
+
337
+ # The elements of a bracketed literal, or nil if this isn't one.
338
+ # Splits on TOP-LEVEL commas only — quote-aware and depth-aware,
339
+ # the same discipline `split_addition` already applies, so a
340
+ # nested array or a comma inside a string element stays whole.
341
+ def array_elements(expr)
342
+ return nil unless expr.start_with?("[") && expr.end_with?("]")
343
+
344
+ inner = expr[1..-2].strip
345
+ return [] if inner.empty?
346
+
347
+ elements = []
348
+ depth = 0
349
+ quote = nil
350
+ current = +""
351
+ inner.each_char do |char|
352
+ if quote
353
+ quote = nil if char == quote
354
+ current << char
355
+ next
356
+ end
357
+ case char
358
+ when '"', "'" then quote = char
359
+ when "[", "(" then depth += 1
360
+ when "]", ")" then depth -= 1
361
+ end
362
+ if char == "," && depth.zero?
363
+ elements << current.strip
364
+ current = +""
365
+ else
366
+ current << char
367
+ end
368
+ end
369
+ elements << current.strip
370
+ elements.reject(&:empty?)
371
+ end
372
+
373
+ # BRACES COUNT TOWARD DEPTH, exactly as parens do — a `+` inside a
374
+ # block predicate's own `{ |x| ... }` body is not this expression's
375
+ # own addition. `parse` tries addition BEFORE `parse_block_opener`,
376
+ # so a paren-only depth count split
377
+ # kings.any? { |k| k.square.file == to.file + 1 && ... }
378
+ # at that inner `+`, turning the whole expression into a nonsense
379
+ # Addition whose left operand then walked "any? { |k| k" into an
380
+ # Array as an attribute path — "TypeError: no implicit conversion
381
+ # of Symbol into Integer", the same signature every unsupported
382
+ # construct raises, which is what let this hide. Found live in a
383
+ # downstream chess domain's castling given; the evaluator's own
384
+ # top_level_index has counted braces since its own version of this
385
+ # exact lesson.
386
+ def split_addition(expr)
387
+ depth = 0
388
+ quote = nil
389
+
390
+ expr.each_char.with_index do |char, index|
391
+ if quote
392
+ quote = nil if char == quote
393
+ elsif ['"', "'"].include?(char)
394
+ quote = char
395
+ elsif ["(", "{"].include?(char)
396
+ depth += 1
397
+ elsif [")", "}"].include?(char)
398
+ depth -= 1
399
+ elsif char == "+" && depth.zero?
400
+ return [expr[0...index].strip, expr[(index + 1)..].strip]
401
+ end
402
+ end
403
+ nil
404
+ end
405
+
406
+ def add(left, right)
407
+ require_number(left, "addition") + require_number(right, "addition")
408
+ end
409
+
410
+ def quoted?(expr)
411
+ return false if expr.length < 2
412
+
413
+ (expr.start_with?('"') && expr.end_with?('"')) ||
414
+ (expr.start_with?("'") && expr.end_with?("'"))
415
+ end
416
+
417
+ # Declared the same way in Vocabulary::SizedType
418
+ # (language/bluebook/vocabulary.bluebook) — spec/vocabulary_conformance_spec
419
+ # holds this equal to the language. Shared by .size and .empty?,
420
+ # which admit the same set for the same reason.
421
+ SIZED_TYPES = Hecks::Vocabulary.fetch("SizedType")
422
+
423
+ def size_of(value)
424
+ return value.size if value.is_a?(Array) || value.is_a?(String) || value.is_a?(Hash)
425
+
426
+ raise EvaluationError, "size expects a list or string, got #{describe(value)}"
427
+ end
428
+
429
+ def emptiness_of(value)
430
+ return value.empty? if value.is_a?(Array) || value.is_a?(String) || value.is_a?(Hash)
431
+
432
+ raise EvaluationError, "empty? expects a list or string, got #{describe(value)}"
433
+ end
434
+
435
+ # `.split("SEP")` -- vendored addition, see the `Split` struct's
436
+ # own comment above. Only a String receiver makes sense to
437
+ # split -- unlike `.length`/`.size`/`.empty?`, which are already
438
+ # meaningful over Array/Hash too, `.split` is a String-only
439
+ # method in the corpus's own usage (every occurrence found this
440
+ # pass splits a Phrase's own string value).
441
+ def split_value(value, separator)
442
+ raise EvaluationError, "split expects a string, got #{describe(value)}" unless value.is_a?(String)
443
+
444
+ value.split(separator)
445
+ end
446
+
447
+ # `.last` -- vendored addition, see the `Last` struct's own
448
+ # comment above. Duck-typed on `respond_to?(:last)` rather than
449
+ # hard-coding Array -- the one corpus usage found this pass
450
+ # (`Query::Phrase`'s `.split("::").last`) always receives a
451
+ # `Split`-produced Array, but nothing about `.last` itself is
452
+ # Array-specific, and this matches `Empty`/`Size`'s own
453
+ # duck-typed-over-a-known-set precedent without inventing a
454
+ # narrower rule than the method needs.
455
+ def last_of(value)
456
+ return value.last if value.respond_to?(:last)
457
+
458
+ raise EvaluationError, "last expects a list, got #{describe(value)}"
459
+ end
460
+
461
+ # `.first` -- see the `First` struct's own comment above.
462
+ # `last_of` with the one method swapped, same duck-typed
463
+ # reasoning.
464
+ def first_of(value)
465
+ return value.first if value.respond_to?(:first)
466
+
467
+ raise EvaluationError, "first expects a list, got #{describe(value)}"
468
+ end
469
+
470
+ # `.start_with?("prefix")` -- vendored addition, see the
471
+ # `StartsWith` struct's own comment above. String-only, same
472
+ # reasoning as `.split` above -- every corpus usage found this
473
+ # pass (`Params`'s own JSON-object-shape invariant) receives a
474
+ # plain String field.
475
+ def starts_with?(value, substring)
476
+ raise EvaluationError, "start_with? expects a string, got #{describe(value)}" unless value.is_a?(String)
477
+
478
+ value.start_with?(substring)
479
+ end
480
+
481
+ # `.end_with?("suffix")` -- vendored addition, see the `EndsWith`
482
+ # struct's own comment above. Same String-only reasoning as
483
+ # `start_with?` immediately above.
484
+ def ends_with?(value, substring)
485
+ raise EvaluationError, "end_with? expects a string, got #{describe(value)}" unless value.is_a?(String)
486
+
487
+ value.end_with?(substring)
488
+ end
489
+
490
+ # Declared the same way in Vocabulary::ToStringType
491
+ # (language/bluebook/vocabulary.bluebook) — spec/vocabulary_conformance_spec
492
+ # holds this equal to the language.
493
+ TO_STRING_TYPES = Hecks::Vocabulary.fetch("ToStringType")
494
+
495
+ def string_of(value)
496
+ case value
497
+ when String then value
498
+ when Integer, Float then value.to_s
499
+ when TrueClass, FalseClass then value.to_s
500
+ when NilClass then ""
501
+ else
502
+ raise EvaluationError, "to_s expects a scalar, got #{describe(value)}"
503
+ end
504
+ end
505
+
506
+ def match_suffix(expr, suffixes)
507
+ suffixes.each do |suffix|
508
+ marker = ".#{suffix}"
509
+ return [expr[0...-marker.length], suffix] if expr.end_with?(marker)
510
+ end
511
+ nil
512
+ end
513
+
514
+ def apply_sign_test(node, value)
515
+ number = numeric(value)
516
+ raise EvaluationError, "#{node.test} expects a number, got #{describe(value)}" unless number
517
+
518
+ Evaluator.apply(node.operator, number, 0)
519
+ end
520
+
521
+ def match_call(expr, marker)
522
+ index = expr.rindex(marker)
523
+ return nil unless index && expr.end_with?(")")
524
+
525
+ [expr[0...index], expr[(index + marker.length)...-1]]
526
+ end
527
+
528
+ def apply_modulo(receiver_value, divisor_value)
529
+ divisor = require_number(divisor_value, "modulo")
530
+ raise EvaluationError, "divided by 0" if divisor.zero?
531
+
532
+ require_number(receiver_value, "modulo").to_i % divisor.to_i
533
+ end
534
+
535
+ def lookup(expr, state, attrs)
536
+ return unwrap_scalar(fetch(expr, state, attrs)) unless expr.include?(".")
537
+
538
+ head, *rest = expr.split(".")
539
+ unwrap_scalar(walk_path(fetch(head, state, attrs), rest))
540
+ end
541
+
542
+ # Walks a list of dotted segments through an already-resolved
543
+ # Hash-like value — extracted from `lookup`'s own reduce so
544
+ # `found_of` (the `Find` node's own path projection) can walk a
545
+ # `.find { ... }`-produced element the identical way `lookup`
546
+ # walks a plain attribute path, rather than duplicating the
547
+ # `value[segment.to_sym] || value[segment]` step twice in this
548
+ # file.
549
+ def walk_path(value, segments)
550
+ segments.reduce(value) do |current, segment|
551
+ break nil unless current.respond_to?(:[])
552
+
553
+ current[segment.to_sym] || current[segment]
554
+ end
555
+ end
556
+
557
+ # `field == "literal"` -- vendored addition, not (yet) upstream
558
+ # hecks (migration plan task 8): the third-most pervasive
559
+ # dispatch-time gap this pass found, same family as `.match?`/
560
+ # `.present?` above -- a lookup of a single-field
561
+ # scalar-convenience value object (the exact shape `Value.
562
+ # from_identifier`/`Value::Coercion#fields_for`'s own single-
563
+ # field auto-unwrap already treats as "this VO IS its scalar"
564
+ # everywhere else in this runtime) came back as the `Value`
565
+ # wrapper itself, never unwrapped for READING -- so `Value#==`
566
+ # (which only ever equals another `Value` instance) silently
567
+ # refused every `guarantees "..." do status == "active" end` /
568
+ # `expects "..." do trash_day.present? end`-shaped bare
569
+ # comparison against a raw literal. Confirmed corpus-wide, not
570
+ # one file's mistake: bin-buddy alone has this exact `field ==
571
+ # "literal"` shape in plan.bluebook, service_task.bluebook,
572
+ # route.bluebook, and subscription.bluebook, all equally silent
573
+ # until a real dispatch (never validate) exercised the
574
+ # predicate. Scoped narrowly to the single-field `{value: X}`
575
+ # shape only.
576
+ #
577
+ # UPDATE 2026-08-18: originally scoped to unwrap ONLY the bare
578
+ # (undotted) case, on the belief that a dotted lookup only ever
579
+ # reaches into a VO's OWN field (`field.value`, `field.sub_
580
+ # field`) and so should keep walking `#[]` untouched. That
581
+ # belief held for the single-hop case but not for the general
582
+ # one: a dotted lookup that NAVIGATES THROUGH an entity/list
583
+ # element to a nested field (`leg.voyage`, where `voyage` is
584
+ # itself a single-field VO) landed on the very same unwrapped-
585
+ # `Value` shape the bare case fixed, and hit the identical
586
+ # silent `Value#==` failure -- comparing it against a raw
587
+ # literal or another unwrapped VO returned false for everything,
588
+ # no error. The terminal value of a dotted walk deserves the
589
+ # same "this VO IS its scalar" treatment as a bare lookup's
590
+ # result; only the INTERMEDIATE hops need raw `#[]` addressing
591
+ # to keep navigating. `unwrap_scalar` is idempotent on an
592
+ # already-raw scalar (a String/Integer doesn't respond to
593
+ # `#to_h`), so this is safe for the existing `field.value`-
594
+ # shaped dotted lookups too -- they already returned a raw
595
+ # scalar and are unaffected.
596
+ def unwrap_scalar(value)
597
+ return value unless value.respond_to?(:to_h) && !value.is_a?(Hash) && !value.is_a?(Array)
598
+
599
+ hash = value.to_h
600
+ hash.size == 1 && hash.key?(:value) ? hash[:value] : value
601
+ end
602
+
603
+ def fetch(name, state, attrs)
604
+ key = name.to_sym
605
+ return attrs[key] if attrs.key?(key)
606
+ return state[key] if known?(state, key)
607
+
608
+ raise EvaluationError, "cannot resolve #{name.inspect} — no such attribute or argument"
609
+ end
610
+
611
+ def known?(state, key)
612
+ return state.key?(key) if state.respond_to?(:key?)
613
+
614
+ !state[key].nil?
615
+ end
616
+
617
+ def numeric(value)
618
+ value if value.is_a?(Integer) || value.is_a?(Float)
619
+ end
620
+
621
+ def require_number(value, operation)
622
+ numeric(value) ||
623
+ raise(EvaluationError, "#{operation} expects a number, got #{describe(value)}")
624
+ end
625
+
626
+ def describe(value) = Rendering.describe(value)
627
+ end
628
+ end
629
+ end
630
+ end
@@ -0,0 +1,13 @@
1
+ module Hecks
2
+ module Bluebook
3
+ # The expression language a bluebook's predicates are written in —
4
+ # canonicalised (canonical_form), resolved against a chapter's names
5
+ # (resolver), and evaluated (evaluator).
6
+ module Expression
7
+ end
8
+ end
9
+ end
10
+
11
+ require_relative "expression/canonical_form"
12
+ require_relative "expression/resolver"
13
+ require_relative "expression/evaluator"