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,351 @@
1
+ module Hecks
2
+ module Bluebook
3
+ module MetaValidator
4
+ # A bluebook rebuilt FROM the meta-domain, in the shape the builder produces.
5
+ #
6
+ # This is the second half of the claim at the top of bluebook.bluebook —
7
+ # "loading a domain becomes dispatching commands into this meta-domain ; the
8
+ # IR it stores must equal the IR the DSL builder produces". The judge is the
9
+ # first half. This reads the records back and assembles `to_h`.
10
+ #
11
+ # It is the INVERSE OF THE WALK and shares its plan: the walk reads a node's
12
+ # lists through the command that appends to each, and this reads them back out
13
+ # of the rows those commands wrote. The retired `experiment/replay.rb` needed
14
+ # 420 hand-written lines because the dispatch half was hand-written too; there
15
+ # is nothing to hand-write when both directions are one table.
16
+ #
17
+ # This file is the TRAVERSAL. The hashes at its tips, and the encodings they
18
+ # undo, are in Shapes.
19
+ #
20
+ # IT READS LEVEL BY LEVEL, THROUGH `DeclaredIn`, AND NOT THROUGH THE READ
21
+ # MODEL — which is the difference between a reconstruction that can be the
22
+ # SOURCE and one that can only be a check.
23
+ #
24
+ # `Meta.whole_bluebook` gathers a chapter in a single read, and it sorts:
25
+ # `ReadModelInterpreter#matching` ends `.sort_by(&:id)` deliberately, because
26
+ # a store's iteration order is an accident, and a read model returning store
27
+ # order would let that accident leak into an answer.
28
+ # Right for a read model, fatal here — the order a bluebook declares its
29
+ # commands in is a FACT ABOUT THE SOURCE, and the IR is a contract field for
30
+ # field and index for index ; the export carries that order verbatim.
31
+ # `DeclaredIn` preserves it (spec/executes_spec says so), so this asks each
32
+ # level for its own children rather than filtering one sorted gather.
33
+ #
34
+ # The parent key of every level is read from the language's own plan, the same
35
+ # Plan the walk dispatches from — so the two directions really are one table,
36
+ # which is what the header above has always claimed.
37
+ #
38
+ # WHAT IT CANNOT REBUILD matters as much as what it can, and
39
+ # spec/round_trip_spec pins the difference as an exact set: a field the language
40
+ # does not hold appears there as a named gap, and a field it stops holding
41
+ # appears as a failure.
42
+ class Reconstruction
43
+ include Readings
44
+ include Shapes
45
+
46
+ def self.of(runtime, chapter) = new(runtime, chapter).to_h
47
+
48
+ def initialize(runtime, chapter)
49
+ @runtime = runtime
50
+ @plan = Plan.for(MetaValidator.grammar_registry)
51
+ @chapter = runtime.query("Bluebook::Bluebook.Called", name: { value: chapter }).first or
52
+ raise NotFound, "the meta-domain holds no bluebook called #{chapter.inspect}"
53
+ end
54
+
55
+ def to_h
56
+ {
57
+ name: text(@chapter[:name]),
58
+ version: text(@chapter[:version]),
59
+ vision: text(@chapter[:vision]),
60
+ classification: text(@chapter[:classification]),
61
+ formerly_known_as: text(@chapter[:formerly_known_as]),
62
+ attaches_to: attached_contexts(@chapter),
63
+ aggregates: declared("Aggregate", chapter_id).map { |row| aggregate(row) },
64
+ read_models: declared("ReadModel", chapter_id).map { |row| read_model(row) },
65
+ policies: declared("Policy", chapter_id).map { |row| policy(row) },
66
+ process_managers: declared("ProcessManager", chapter_id).map { |row| process_manager(row) }
67
+ }
68
+ end
69
+
70
+ private
71
+
72
+ def chapter_id = @chapter[:id].to_s
73
+
74
+ # Everything DECLARED IN one parent, in the order it was declared. The key
75
+ # is the one the language's own creating command carries, read from Plan.
76
+ def declared(category, parent_id)
77
+ key = @plan.category(category).parent_key
78
+ @runtime.query("Bluebook::#{category}.DeclaredIn", key.to_sym => { value: parent_id.to_s })
79
+ end
80
+
81
+ # ONE DECLARATION, BUILT FROM THE CONTRACT.
82
+ #
83
+ # There were eleven methods here, one per category, each spelling out the same
84
+ # keys `Assembly::Contracts` already names — which is the shape the judge used
85
+ # to have and paid fourteen unoffered verbs for. The keys come from the table
86
+ # now, and the exceptions come from its `reads:` column: a list needs a reader
87
+ # per element, a folded field is gathered rather than fetched, and everything
88
+ # else is one cell.
89
+ #
90
+ # `extra` is what the containment supplies — children, and the folded objects
91
+ # a row cannot hold on its own.
92
+ def declaration(category, row, extra = {})
93
+ contract = Assembly.contract(category)
94
+
95
+ contract.fields.each_with_object({}) do |(_keyword, (key, _build)), out|
96
+ next if extra.key?(key)
97
+
98
+ out[key] = read_row(contract.reader(key), key, row)
99
+ end.merge(extra)
100
+ end
101
+
102
+ def read_row(spec, key, row)
103
+ case spec
104
+ when nil then text(row[key])
105
+ when :symbol then text(row[key])&.to_sym
106
+ when :names then Array(row[key]).map { |held| text(held[:name]) }
107
+ when Array then read_shaped(spec, key, row)
108
+ else send(spec, row)
109
+ end
110
+ end
111
+
112
+ # The readers are the ones `Shapes` already has, called by name — no wrappers,
113
+ # so nothing shadows them. `:each_with_id` is the single exception that needs
114
+ # more than the element: an attribute's type came in as the ID of whatever it
115
+ # names, so reading it back needs the row it belongs to.
116
+ def read_shaped(spec, key, row)
117
+ shape, named = spec
118
+
119
+ case shape
120
+ when :each then Array(row[key]).map { |held| send(named, held) }
121
+ when :each_with_id then Array(row[key]).map { |held| send(named, held, row[:id]) }
122
+ when :call then send(named, row)
123
+ when :from then pairs(row[named])
124
+ end
125
+ end
126
+
127
+ def pairs(with) = Array(with).map { |binding| [text(binding[:key]), text(binding[:value])] }
128
+
129
+ # THE PARTS, IN THE ORDER THEY WENT IN, because the identity is their join
130
+ # and a join read out of order names a different record.
131
+ def identity_paths(row) = Array(row[:identified_by]).map { |part| text(part[:value]).to_s }
132
+
133
+ # THE CONTEXTS ONE CHAPTER NAMES ITSELF ONTO, in the order they were
134
+ # attached — same shape identity_paths reads back, one level up.
135
+ def attached_contexts(row) = Array(row[:attaches_to]).map { |part| text(part[:value]).to_s }
136
+
137
+ # Every cell of the meta-domain is a single-field value object, so a row
138
+ # arrives holding Values rather than Strings.
139
+ def text(cell)
140
+ return nil if cell.nil?
141
+ return cell.to_h.values.first if cell.respond_to?(:to_h) && !cell.is_a?(String)
142
+
143
+ cell
144
+ end
145
+
146
+ # An aggregate's OWN verbs and asks — the ones no entity declared. Both carry
147
+ # the parent link either way, because that is the head the reference resolves
148
+ # against, so the entity ones have to be told apart by `entity_id`. Rejecting
149
+ # from an ORDERED read keeps the order.
150
+ def own(category, aggregate_id)
151
+ declared(category, aggregate_id).reject { |row| text(row[:entity_id]).to_s != "" }
152
+ end
153
+
154
+ # A piece's own verbs and asks. There is no `DeclaredIn` keyed by entity, so
155
+ # this reads the aggregate's — in declaration order — and keeps the ones that
156
+ # name this piece. `row[:aggregate]` — an ENTITY row's own `reference_to
157
+ # Aggregate` (bare, no `_id` since ADR 0025) — not `entity_id`, which
158
+ # STAYS suffixed below : that one is Command/Query's own EXPLICIT `as:`,
159
+ # never touched by the rename.
160
+ def within(category, row)
161
+ declared(category, text(row[:aggregate]))
162
+ .select { |held| text(held[:entity_id]).to_s == row[:id].to_s }
163
+ end
164
+
165
+ def aggregate(row)
166
+ id = row[:id]
167
+
168
+ {
169
+ name: text(row[:name]),
170
+ description: text(row[:description]),
171
+ identified_by: identity_paths(row),
172
+ attributes: Array(row[:attributes]).map { |field| attribute(field, id) },
173
+ value_objects: declared("ValueObject", id).map { |shape| value_object(shape) },
174
+ commands: own("Command", id).map { |verb| command(verb) },
175
+ # THE AGGREGATE BOUNDARY, and the precondition a command may
176
+ # reference by name (S10, ADR 0025 — "Rules") — the same
177
+ # `rule` reader `command`'s own givens/ensures already use
178
+ # (Assembly::Contracts' generic `declaration()` path), read
179
+ # here by hand because `aggregate(row)` itself is hand-typed,
180
+ # unlike `command`/`value_object`/`query` below.
181
+ invariants: Array(row[:invariants]).map { |held| rule(held) },
182
+ preconditions: Array(row[:preconditions]).map { |held| rule(held) },
183
+ # S12, ADR 0025 — same reason invariants/preconditions are
184
+ # read by hand two lines up: `aggregate(row)` is hand-typed.
185
+ projected_fields: Array(row[:projected_fields]).map { |held| projected_field(held) },
186
+ lifecycle: lifecycle(row),
187
+ entities: direct_entities(id, id).map { |piece| entity(piece) },
188
+ queries: own("Query", id).map { |ask| query(ask) },
189
+ provenance: provenance(row)
190
+ }
191
+ end
192
+
193
+ def value_object(row) = declaration("ValueObject", row)
194
+
195
+ def closed_set_of(row) = !text(row[:rows]).nil?
196
+
197
+ # A closed set's admitted rows. S17, ADR 0026 — Member is a genuine
198
+ # ENTITY now (`entity "Member" do ... end`, nested under
199
+ # ValueObject), created by `ValueObject.Member` (an ordinary append,
200
+ # the same way `Account.LogEntry` creates a LedgerEntry) and mutated
201
+ # by its own dotted `ValueObject.Member.Pair`. Its data therefore
202
+ # lives INLINE on the value object's own dispatched state — same as
203
+ # any other entity list — not behind a separate `DeclaredIn` query:
204
+ # there is no such query any more, because there is no top-level
205
+ # "Member" aggregate left to hold one. Pairs are still an OPEN MAP,
206
+ # which no value object can hold, so they still come back one pair
207
+ # at a time.
208
+ def members_row(row) = members_of(row)
209
+
210
+ # THE KEY IS STRINGIFIED, NEVER THE VALUE — the same split
211
+ # `Bluebook::ValueObject#to_h`'s own `members:` emission makes
212
+ # (lib/hecks/bluebook/value_object.rb, L7 docs/audits/
213
+ # 2026-08-11-bug-triage.md). `Pair.value` is declared `String`
214
+ # (shape.bluebook), but a real pair can hold whatever native Ruby
215
+ # type the source member line actually wrote (`StatementFrequency`'s
216
+ # own `retention_months: 84`, examples/banking/bluebook/
217
+ # statements.bluebook) — `text` already unwraps the stored Value to
218
+ # that real type, so calling `.to_s` here erased it a second time,
219
+ # on the side spec/round_trip_spec.rb compares straight against a
220
+ # fresh raw load (`Reconstruction.of` directly, no Assembly in
221
+ # between): `declared 84, read back "84"` the moment `to_h` stopped
222
+ # erasing it on the OTHER side and this one kept erasing it alone.
223
+ def members_of(value_object_row)
224
+ Array(value_object_row[:members]).map do |member|
225
+ Array(member[:pairs]).map { |pair| [text(pair[:key]).to_s, text(pair[:value])] }
226
+ end
227
+ end
228
+
229
+ def command(row) = declaration("Command", row)
230
+
231
+ def query(row) = declaration("Query", row).merge(options_of(row))
232
+
233
+ # EVERY DIRECT ENTITY OF ONE OWNER — S17, ADR 0026. `declared
234
+ # ("Entity", root_id)` returns EVERY entity sharing the same
235
+ # ROOT aggregate, nested or not (Dispatch and Handler both carry
236
+ # `aggregate: process_manager_id`) — `owner` is the field that
237
+ # actually tells them apart : `Judge#nest_entities`'s own
238
+ # comment explains why it is the one to repurpose. Called with
239
+ # `owner_id == root_id` for an aggregate's own direct entities
240
+ # (Handler), and with `owner_id == some entity's own id` for
241
+ # THAT entity's own nested ones (Dispatch, owned by Handler).
242
+ def direct_entities(root_id, owner_id)
243
+ declared("Entity", root_id).select { |held| text(held[:owner]).to_s == owner_id.to_s }
244
+ end
245
+
246
+ def entity(row)
247
+ {
248
+ name: text(row[:name]),
249
+ description: text(row[:description]),
250
+ # The same shape as an aggregate's now. It was a String here and a
251
+ # Symbol there — the IR was not uniform about it, and only a round trip
252
+ # ever said so.
253
+ identified_by: identity_paths(row),
254
+ # `text(row[:aggregate])` — the OWNING aggregate, the same one
255
+ # `Judge#owning_aggregate_ref` resolved this piece's own
256
+ # attribute types against on the way in, so reconstruction reads
257
+ # a value-object-typed attribute back the identical way an
258
+ # aggregate's own is (`Shapes#shape_field`'s own comment).
259
+ attributes: Array(row[:attributes]).map { |field| shape_field(field, text(row[:aggregate])) },
260
+ # ADR 0028 — the SAME shape `aggregate(row)`'s own
261
+ # `preconditions:` reads two hand-typed methods up, read by
262
+ # hand for the identical reason: `entity(row)` is hand-typed
263
+ # too, unlike `command`/`value_object`/`query`.
264
+ preconditions: Array(row[:preconditions]).map { |held| rule(held) },
265
+ # Round 7 — the SAME shape `aggregate(row)`'s own
266
+ # `invariants:` reads, one level down: a piece's own shape
267
+ # rule, checked against every instance of this piece.
268
+ invariants: Array(row[:invariants]).map { |held| rule(held) },
269
+ commands: within("Command", row).map { |verb| command(verb) },
270
+ queries: within("Query", row).map { |ask| query(ask) },
271
+ # S17, ADR 0026 — Dispatch, inside Handler : an entity's own
272
+ # NESTED entities, found the same way its own direct ones
273
+ # were one level up.
274
+ entities: direct_entities(text(row[:aggregate]), row[:id]).map { |piece| entity(piece) },
275
+ # An entity has its own state machine, and the language has held it all
276
+ # along — Entity.Lifecycle and Entity.Transition were two of the fourteen
277
+ # verbs that started firing when the judge became a walk. Only the
278
+ # reconstruction had never asked for them.
279
+ lifecycle: lifecycle(row)
280
+ }
281
+ end
282
+
283
+ # Assembled from three fields, because the IR keeps one object where the
284
+ # language keeps the parts — the same difference Readings holds for the walk,
285
+ # in the other direction.
286
+ def lifecycle(row)
287
+ field = text(row[:state_field])
288
+ return nil if field.to_s.empty?
289
+
290
+ {
291
+ field: field,
292
+ default: text(row[:state_start]),
293
+ transitions: Array(row[:transitions]).map { |move| transition(move) }
294
+ }
295
+ end
296
+
297
+ def policy(row) = declaration("Policy", row)
298
+
299
+ # S17, ADR 0026 — Handler is a genuine entity now, nested under
300
+ # ProcessManager, created by `ProcessManager.Handler` (an
301
+ # ordinary append, the same way `Account.LogEntry` creates a
302
+ # LedgerEntry) and mutated by its own dotted `ProcessManager.
303
+ # Handler.Dispatch`/`...Dispatch.Bind`. Its data therefore lives
304
+ # INLINE on the process manager's own dispatched state — same as
305
+ # any other entity list — not behind a separate `DeclaredIn`
306
+ # query : there is no such query any more, the same fix
307
+ # `Reconstruction#members_of` already made for Member.
308
+ def process_manager(row)
309
+ declaration("ProcessManager", row,
310
+ handlers: Array(row[:handlers]).map { |leg| handler(leg) })
311
+ end
312
+
313
+ # S17, ADR 0026 — Dispatch, one level further in : nested under
314
+ # Handler, its data lives inline on the HANDLER row this method
315
+ # was just handed (`row` here IS one element of `handlers`,
316
+ # above), not behind any query either.
317
+ def handler(row)
318
+ declaration("Handler", row,
319
+ dispatches: Array(row[:dispatches]).map { |leg| dispatch(leg) })
320
+ end
321
+
322
+ # `compensates` — TWO FLAT FIELDS on this same row
323
+ # (`process_manager.bluebook`'s own comment on `Dispatch` for
324
+ # why), assembled BY HAND into the shape its own field actually
325
+ # is — `declaration()`'s generic per-field hash-build has no
326
+ # way to turn two cells into a second object, so this reads
327
+ # them directly and passes the result through `extra:`, the
328
+ # same seam `handler`/`process_manager` already use for a
329
+ # `:children` shape a flat Contract cannot describe.
330
+ # `compensates_command_name` absent means no compensation at
331
+ # all — a plain dispatch with nothing to undo. A PLAIN HASH, the
332
+ # SAME declaration shape `to_h` spells for everything else in
333
+ # this file (this file's own top comment) — never a real
334
+ # `DispatchSpec` here; `Assembly#dispatch` is the one place a
335
+ # declaration hash becomes the real object, and building it
336
+ # twice, in two different shapes, is exactly the kind of drift
337
+ # this whole arc exists to remove.
338
+ def dispatch(row)
339
+ name = text(row[:compensates_command_name])
340
+ compensates = name && { command_name: name, with_spec: pairs(row[:compensates_with_spec]) }
341
+
342
+ declaration("Dispatch", row, compensates: compensates)
343
+ end
344
+
345
+ def read_model(row)
346
+ declaration("ReadModel", row).merge(query_name: text(row[:query_name])).merge(options_of(row))
347
+ end
348
+ end
349
+ end
350
+ end
351
+ end
@@ -0,0 +1,266 @@
1
+ module Hecks
2
+ module Bluebook
3
+ module MetaValidator
4
+ # The leaf shapes of a reconstructed bluebook, and the encodings that go with
5
+ # them.
6
+ #
7
+ # Reconstruction walks the tree; this rebuilds the small hashes at its tips —
8
+ # an attribute, a rule, a where-clause, a transition — and undoes the two
9
+ # things the walk encodes on the way in: an attribute's type offered as the ID
10
+ # of whatever it names, and an append flattened to one row per binding.
11
+ #
12
+ # Separate from the traversal because they are different concerns, and because
13
+ # the two together were 211 code lines against a 200 limit.
14
+ module Shapes
15
+ # The type came in as the id of what it names, so it goes back out as the
16
+ # name — or, for another aggregate's head, as the encoding the IR spells.
17
+ def attribute(field, aggregate_id)
18
+ type = text(field[:type]).to_s
19
+
20
+ {
21
+ name: text(field[:name])&.to_sym,
22
+ type: owned_type(type, aggregate_id) || reference_type(type),
23
+ list: text(field[:list]).to_s == "true",
24
+ default: decode_literal(text(field[:default])),
25
+ # Read back the same way `list` is — both are booleans about the
26
+ # attribute, held as text, and dropping either would rebuild a
27
+ # bluebook that no longer says what it said.
28
+ optional: text(field[:optional]).to_s == "true",
29
+ pattern: presence(text(field[:pattern])),
30
+ # THE ROUND TRIP IS THE ONLY WAY IN. The grammar registry keeps the
31
+ # ASSEMBLED graph — the language as its own judge read it back — so a
32
+ # fact dropped here is a fact no downstream projection ever sees, no
33
+ # matter how plainly the .bluebook file declares it.
34
+ admits: presence(text(field[:admits])),
35
+ relationship: presence(text(field[:relationship]))
36
+ }
37
+ end
38
+
39
+ # A TYPE THIS AGGREGATE OWNS, offered as its id and read back as its name.
40
+ # Prefixed with the owner and the identity join, because that IS the value
41
+ # object's identity — the aggregate it belongs to, then its name.
42
+ def owned_type(type, aggregate_id)
43
+ prefix = Naming.identity([aggregate_id, ""])
44
+ return nil unless type.start_with?(prefix)
45
+
46
+ type.delete_prefix(prefix)
47
+ end
48
+
49
+ # ONE PART OF AN IDENTITY, read back as the path it went in as. The inverse
50
+ # of `Marks#identity_path`, and named the same so the two directions read
51
+ # as one table.
52
+ def identity_path(part) = text(part[:value]).to_s
53
+
54
+ # An argument, a parameter, a piece's attribute — the three places an
55
+ # attribute is written that carry no owner id to strip... except a
56
+ # piece's own attribute now does : an entity is its own root (repeating
57
+ # the aggregate's whole shape one level down), so ITS attributes name a
58
+ # value object exactly the way an aggregate's own do (`Judge#cell`'s own
59
+ # comment), and reconstruction has to undo the same owned-vs-reference
60
+ # split `attribute`, below, already undoes for an aggregate — not the
61
+ # cruder "any qualified name is a Reference" guess this used before,
62
+ # which mistook a piece's own value-object type for a cross-aggregate
63
+ # head every time.
64
+ #
65
+ # `aggregate_id`, passed only for a piece's own attribute (an
66
+ # argument or a parameter carries no value-object type at all — see
67
+ # entity.bluebook's/command.bluebook's own comments on why — so the
68
+ # generic reader path calls this with one argument and gets the
69
+ # OLD reading unchanged), is the piece's OWNING aggregate — the same
70
+ # one its own attribute types were resolved against on the way in
71
+ # (`Judge#owning_aggregate_ref`).
72
+ #
73
+ # A REFERENCE among the other two came in as the head's ID, because
74
+ # that is what Command/Query.Reference offer, so it goes back out as
75
+ # the encoding the IR spells. A qualified name is the tell : an
76
+ # ordinary type names something declared beside it (`Money`,
77
+ # `AccountNumber`) and never carries a chapter, while a head's id
78
+ # always does.
79
+ def shape_field(field, aggregate_id = nil)
80
+ return attribute(field, aggregate_id) if aggregate_id
81
+
82
+ type = text(field[:type]).to_s
83
+
84
+ {
85
+ name: text(field[:name])&.to_sym,
86
+ # A QUALIFIED name is the tell : an ordinary type names something
87
+ # declared beside it (`Money`, `AccountNumber`) and carries no join at
88
+ # all, where a head's id always does (chapter + name, joined the way
89
+ # every derived id is).
90
+ type: type.include?(Naming::IDENTITY_JOIN) ? reference_type(type) : text(field[:type]),
91
+ list: text(field[:list]).to_s == "true",
92
+ default: decode_literal(text(field[:default])),
93
+ optional: text(field[:optional]).to_s == "true",
94
+ pattern: presence(text(field[:pattern])),
95
+ admits: presence(text(field[:admits])),
96
+ relationship: presence(text(field[:relationship]))
97
+ }
98
+ end
99
+
100
+ # An ABSENT pattern is nil, not "". The language holds every field as
101
+ # text, so a field nobody set comes back as the empty string — and ""
102
+ # is a real regex (it matches everything), so keeping it would turn
103
+ # "no pattern" into "a pattern that always passes" and quietly cost the
104
+ # IR its round trip.
105
+ def presence(text)
106
+ value = text.to_s
107
+ value.empty? ? nil : value
108
+ end
109
+
110
+ # A literal, read back from the self-describing form Readings#encode_literal
111
+ # wrote — the same reader Assembly::Marks uses, because it is the same
112
+ # spelling. The forms are exactly the five the Primitive vocabulary admits —
113
+ # String, Integer, Float, TrueClass, FalseClass — plus a symbol, and an
114
+ # object literal, which is what `to: { value: "good" }` is: a value object's
115
+ # fields written inline.
116
+ def decode_literal(text) = Literal.read(text)
117
+
118
+ def rule(row) = { description: text(row[:description]), canonical: text(row[:canonical]) }
119
+
120
+ # S12, ADR 0025 — `projects :name, from: :"reference.remote_field"`,
121
+ # read back the same three plain identifiers `rule` above reads
122
+ # description/canonical as.
123
+ def projected_field(row)
124
+ { name: text(row[:name]), reference: text(row[:reference]), remote_field: text(row[:remote_field]) }
125
+ end
126
+
127
+ # `provenance from: {...}` rides the same literal encoding `default:`
128
+ # does — an object literal, self-describing via Hecks::Literal — one
129
+ # level up: a whole keyword's argument rather than an attribute's
130
+ # `default:`.
131
+ def provenance(row) = decode_literal(text(row[:provenance]))
132
+
133
+ # `command "Debit", from: "open"` — the SAME literal encoding
134
+ # `provenance`/`default:` already ride (S10, ADR 0025), one
135
+ # state or an array of them, or nil for a command with no
136
+ # lifecycle guard.
137
+ def from(row) = decode_literal(text(row[:from]))
138
+
139
+ # THE OPTION ROWS, GATHERED BACK into the shapes `extra_options_to_h` spells.
140
+ #
141
+ # One row per part, so a compound option is several rows and a repeated one is
142
+ # several groups told apart by `at`. Grouping by option name and then by `at`
143
+ # rebuilds both without either knowing which options exist — the whole point
144
+ # of holding them as an open map.
145
+ def options_of(row)
146
+ Array(row[:options])
147
+ .group_by { |part| text(part[:option]) }
148
+ .to_h { |option, parts| [option.to_sym, gathered(parts)] }
149
+ end
150
+
151
+ def gathered(parts)
152
+ repeated, single = parts.partition { |part| !text(part[:at]).to_s.empty? }
153
+ return single.to_h { |part| [text(part[:key]).to_sym, text(part[:value])] } if repeated.empty?
154
+
155
+ repeated.group_by { |part| text(part[:at]) }
156
+ .values
157
+ .map { |group| group.to_h { |part| [text(part[:key]).to_sym, text(part[:value])] } }
158
+ end
159
+
160
+ # The IR keeps a where's field as a STRING, not a symbol — it is read back
161
+ # out, never called. The value stays RAW TEXT here on purpose : this
162
+ # feeds the declaration hash Assembly::Marks#where_clause decodes
163
+ # from (via `read`), and decoding twice is worse than once — a
164
+ # kwarg reference (":ceiling") decoded here into the Symbol :ceiling
165
+ # would have its colon stripped by `read`'s own `.to_s` and come
166
+ # back out as the plain string "ceiling", indistinguishable from a
167
+ # literal of the same name. One decode, at the one place that builds
168
+ # the object every comparator actually reads.
169
+ def where_clause(row)
170
+ { field: text(row[:field]), op: text(row[:op]), value: text(row[:value]) }
171
+ end
172
+
173
+ # One object in the IR, two fields in the language.
174
+ def order_by(row)
175
+ field = text(row[:order_field])
176
+ return nil if field.to_s.empty?
177
+
178
+ { field: field, direction: text(row[:order_way]) }
179
+ end
180
+
181
+ def limit(row)
182
+ ceiling = text(row[:limit])
183
+ return nil if ceiling.to_s.empty?
184
+
185
+ { value: ceiling }
186
+ end
187
+
188
+ def transition(row)
189
+ {
190
+ command: text(row[:command]),
191
+ from_state: text(row[:from_state]),
192
+ to_state: text(row[:to_state])
193
+ }
194
+ end
195
+
196
+ def head(row)
197
+ {
198
+ aggregate: text(row[:aggregate]),
199
+ # A String, like an entity's identified_by. The IR is not uniform about
200
+ # this and only a round trip says so.
201
+ as: text(row[:as]),
202
+ many: text(row[:many]).to_s == "true"
203
+ }
204
+ end
205
+
206
+ def group_by_field(row) = { field: text(row[:field]) }
207
+
208
+ # `count`'s own boolean, read back the SAME way `head`'s own
209
+ # `many` is (`text(row[:many]).to_s == "true"`) — except a
210
+ # `ReadModel.Count` command is dispatched AT ALL only when
211
+ # `@count` was truthy (`MetaValidator::Judge#setters` skips a
212
+ # setter whose every source is absent), so an undeclared read
213
+ # model's own `count` field never gets written and comes back
214
+ # `nil` here, never `"false"` — matching `ReadModel#to_h`'s own
215
+ # `true`/`nil` pair (never `false`) exactly, rather than the
216
+ # unconditional `true`/`false` `head`'s own `many` needs (every
217
+ # head DOES get a `Gather` dispatch, declared or derived).
218
+ def read_model_count(row) = (true if text(row[:count]).to_s == "true")
219
+
220
+ # THE APPEND FLATTENING, IN REVERSE.
221
+ #
222
+ # An append binds several fields at once and the language's Change holds one
223
+ # field/kind/source triple, so the walk offers an append once PER BINDING.
224
+ # Rebuilding groups those rows back into the single mutation the IR keeps —
225
+ # the only place here that undoes something rather than simply reading it.
226
+ def mutations(row)
227
+ Array(row[:mutations])
228
+ .group_by { |change| [text(change[:target]), text(change[:op])] }
229
+ .map { |(target, op), bindings| mutation(target, op, bindings) }
230
+ end
231
+
232
+ # `sign:` — read the SAME way the declared side computes it
233
+ # (`Bluebook::Mutation.sign_for`, item #5 of the whole-project
234
+ # table-unification survey), not stored on any row here — the
235
+ # meta-domain's own Change entity carries no `sign` field of its
236
+ # own (it is a pure function of `op`, nothing to persist), so
237
+ # reconstruction recomputes it the same way a freshly-built
238
+ # Mutation's own `to_h` lambda does, rather than leaving the key
239
+ # silently absent (spec/round_trip_spec's whole point: a field the
240
+ # language does not hold is a named gap, not a byte-for-byte one).
241
+ def mutation(target, op, bindings)
242
+ base = { target: target.to_sym, op: op.to_sym, sign: Hecks::Bluebook::Mutation.sign_for(op) }
243
+ # `:delegate`/`:corrects` (CommandBuilder#delegates_to's and
244
+ # #corrects_impl's own comments) ride the SAME multi-binding
245
+ # shape `:append` does.
246
+ return base.merge(fields: appended(bindings)) if ["append", "delegate", "corrects"].include?(op)
247
+
248
+ base.merge(source: classified(bindings.first))
249
+ end
250
+
251
+ def appended(bindings)
252
+ bindings.to_h { |binding| [text(binding[:field]).to_sym, text(binding[:source])] }
253
+ end
254
+
255
+ def classified(binding)
256
+ kind = text(binding[:kind])
257
+ value = text(binding[:source])
258
+
259
+ return { kind: kind, name: value } if %w[argument state].include?(kind)
260
+
261
+ { kind: "literal", value: decode_literal(value) }
262
+ end
263
+ end
264
+ end
265
+ end
266
+ end