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,65 @@
1
+ require_relative "../caller"
2
+ require_relative "../errors"
3
+ require_relative "../refusal_wording"
4
+ require_relative "../../ports/authorization"
5
+
6
+ module Hecks
7
+ module Runtime
8
+ class CommandRules
9
+ # Whether the caller may run this command at all — a `role` mismatch,
10
+ # the one check that runs before any domain-state work, alongside the
11
+ # argument gate rather than after it.
12
+ #
13
+ # TWO CHECKS, NOT A REPLACEMENT — ADR 0025 §9's own caution against
14
+ # "silently downgrading `role` to documentation" cuts both ways: a
15
+ # caller who never named WHO they are (every caller before this) is
16
+ # checked exactly the way it always has been, string equality
17
+ # against the command's own `role`. Only a caller that ALSO binds an
18
+ # `actor_id` (`Hecks.as_caller(role:, actor_id:)`) reaches the
19
+ # real check — a live Governance::RoleAssignment lookup through
20
+ # `Ports::Authorization`, once the command's domain declares
21
+ # `uses_framework "Governance"` (`HecksagonBuilder` refuses any
22
+ # domain that declares a `role` and does not, at build time — see
23
+ # its own `refuse_ungoverned_roles!`). An identified caller is never
24
+ # let back through the string fallback: a real identity that holds
25
+ # no matching grant is refused, not waved through because it also
26
+ # happens to type the right word.
27
+ module Authorization
28
+ # OPT-IN, on BOTH sides. No caller bound: unchecked, exactly as
29
+ # today. No role declared: unchecked too — `role` is genuinely
30
+ # optional in this language (roughly a third of banking's own
31
+ # commands declare none), so a command that never named a role has
32
+ # nothing to check a caller against.
33
+ def refuse_role_mismatch(command, domain)
34
+ caller = Caller.current
35
+ return unless caller
36
+ return if command.role.to_s.empty?
37
+
38
+ authorized =
39
+ if caller.actor_id && governance_attached?(domain)
40
+ Ports::Authorization.holds_role?(registry, actor_id: caller.actor_id, role: command.role,
41
+ as_of: caller.as_of, scope: caller.scope)
42
+ else
43
+ caller.role == command.role
44
+ end
45
+
46
+ return if authorized
47
+
48
+ raise Unauthorized, RefusalWording.render("Unauthorized", "role_mismatch",
49
+ command: command.hecks_name, role: command.role,
50
+ caller_role: caller.role)
51
+ end
52
+
53
+ private
54
+
55
+ # SELF-EXEMPT, like `HecksagonBuilder#refuse_ungoverned_roles!` —
56
+ # Governance IS the thing a `holds_role?` lookup runs against, so
57
+ # its own commands are always checked by the string fallback, not
58
+ # a lookup against itself.
59
+ def governance_attached?(domain)
60
+ domain.to_s == "Governance" || registry.hecksagon(domain)&.framework_members&.include?("Governance") || false
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,34 @@
1
+ require_relative "../event"
2
+
3
+ module Hecks
4
+ module Runtime
5
+ class CommandRules
6
+ # What a command's `emits` becomes: an Event in the registry's log,
7
+ # and — where the store can hold one — a recorded event beside the
8
+ # data it describes.
9
+ module Emission
10
+ # `correlation` arrives HERE rather than being merged onto the
11
+ # event afterwards. It is part of the transaction — known from
12
+ # `dispatch`'s own argument before anything is emitted — and an
13
+ # event that is still being written to is not yet a record of
14
+ # what happened. Setting it at construction is what lets the
15
+ # event be frozen the moment it exists.
16
+ def emit(command, domain, aggregate, instance, args, repository, correlation = nil)
17
+ command.emits.map do |event_name|
18
+ event = Event.new(
19
+ name: event_name,
20
+ aggregate: "#{domain}::#{aggregate.hecks_name}",
21
+ id: instance.id,
22
+ payload: args,
23
+ occurred_at: Time.now.utc.iso8601,
24
+ correlation: correlation
25
+ )
26
+ @registry.event_log << event.emit!
27
+ repository.record_event(event) if repository.respond_to?(:record_event)
28
+ event
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,189 @@
1
+ require_relative "../errors"
2
+ require_relative "../refusal_wording"
3
+ require_relative "../value"
4
+
5
+ module Hecks
6
+ module Runtime
7
+ class CommandRules
8
+ # A reference must point at something that EXISTS.
9
+ #
10
+ # `reference_to Customer` is the one guarantee an aggregate reference is
11
+ # for, and it was declared 14 times across banking and enforced nowhere :
12
+ # an Account could belong to a customer who was never registered, and
13
+ # every gate stayed green because
14
+ # no corpus step ever passed a dangling reference.
15
+ module References
16
+ # Resolved here rather than in coercion because coercion is pure — it
17
+ # holds no repository. A reference INTO ANOTHER DOMAIN is left alone : a
18
+ # cross-domain target may legitimately not be loaded, which is the same
19
+ # reading `across` policies already get.
20
+ #
21
+ # Shared by CommandInterpreter and EntityInterpreter — an entity command
22
+ # can declare a reference-typed attribute the same way an aggregate
23
+ # command can, even though nothing in the real corpus does yet.
24
+ def resolve_references(domain, command, args)
25
+ command.attributes.each do |attribute|
26
+ next unless attribute.reference?
27
+ next unless args.key?(attribute.name)
28
+
29
+ held = args[attribute.name]
30
+ next if held.nil?
31
+
32
+ target = referenced_aggregate(attribute)
33
+ next unless target
34
+
35
+ validate_reference_values(domain, target, held, list: attribute.list?)
36
+ end
37
+ end
38
+
39
+ # Structural references are checked again against the settled state.
40
+ # This is what makes a `has_many` declared on an aggregate honest even
41
+ # when a command supplies its list through an ordinary typed argument.
42
+ def resolve_state_references(domain, construct, state)
43
+ construct.attributes.each do |attribute|
44
+ next unless attribute.reference?
45
+
46
+ held = state[attribute.name]
47
+ validate_relationship_cardinality(construct, attribute, held)
48
+ next if held.nil?
49
+
50
+ target = referenced_aggregate(attribute)
51
+ next unless target
52
+
53
+ validate_reference_values(domain, target, held, list: attribute.list?)
54
+ end
55
+
56
+ Array(construct.entities).each do |entity|
57
+ field = construct.attribute(Naming.snake(entity.hecks_name).to_sym) ||
58
+ construct.attributes.find { |attribute| attribute.type.to_s == entity.hecks_name.to_s }
59
+ next unless field
60
+
61
+ Array(state[field.name]).each { |row| resolve_state_references(domain, entity, row) }
62
+ end
63
+ end
64
+
65
+ # `has_one` and `belongs_to` mean exactly one target unless the
66
+ # declaration explicitly says optional. State validation owns this
67
+ # boundary because a command may leave an aggregate field untouched;
68
+ # checking only command arguments would let a required relationship be
69
+ # persisted as nil. `has_many` admits zero members, so its empty list is
70
+ # already a valid cardinality and needs no presence refusal.
71
+ def validate_relationship_cardinality(construct, attribute, held)
72
+ return if attribute.relationship.nil? || attribute.list?
73
+ return unless held.nil? && !attribute.optional?
74
+
75
+ raise TypeMismatch,
76
+ "#{construct.hecks_name}.#{attribute.name} is a required " \
77
+ "#{attribute.relationship} relationship — expected one " \
78
+ "#{attribute.type.target_name} identity, got nil"
79
+ end
80
+
81
+ # The reference RESOLVES itself — through the chapter's own IR, so the
82
+ # bluebook's declared heads are the index. This used to regex the target's
83
+ # name out of "Reference<Customer>" and then search
84
+ # `registry.bluebook(domain).aggregates` for it — and later reached the
85
+ # target through Ruby's constant tree, a class thrown away for its `.ir`
86
+ # the moment it was found.
87
+ def referenced_aggregate(attribute)
88
+ attribute.type.resolve
89
+ end
90
+
91
+ def validate_reference_values(domain, target, held, list:)
92
+ values = list ? Array(held) : [held]
93
+ values.each do |value|
94
+ key = reference_key(value)
95
+ next if key.empty?
96
+ next if @registry.repository(domain, target).find(key)
97
+
98
+ raise NotFound,
99
+ RefusalWording.render("NotFound", "reference_target_missing",
100
+ target: target.name, heads: target.identity_heads.join(", "),
101
+ key: key.inspect)
102
+ end
103
+ end
104
+
105
+ # `value` is the referenced record's own id, EXACTLY as `Identity.of`
106
+ # would build it for that record — a bare scalar for a single-field
107
+ # identity (the overwhelming common case; Banking's own plain
108
+ # `reference_to Customer` holds one already, so `Value.
109
+ # materialize_unwrapped` is a no-op passthrough here), or a
110
+ # `Naming.identity`-joined string for a compound one
111
+ # (`belongs_to Translation, as: :translation_ref` — Translation's
112
+ # own `identified_by :domain, :from, :to`, THREE fields). Before
113
+ # this, plain `value.to_s` on that compound case's own coerced
114
+ # Value hit Ruby's default `Object#to_s` (a raw, run-to-run-random
115
+ # memory address) instead of joining the record's real id — found
116
+ # live via bin/fuzz on the self-hosted "translation" domain
117
+ # (replay_is_deterministic), the SAME class of gap `Identity.from`
118
+ # already had for a compound `identified_by`'s own bare (undotted)
119
+ # attribute paths. `materialize_unwrapped` recurses a multi-
120
+ # attribute value object to a plain Hash keyed by attribute name,
121
+ # in declaration order — `Naming.identity` on `.values` reproduces
122
+ # the identical join `Identity.of` itself would produce for the
123
+ # SAME fields.
124
+ def reference_key(value)
125
+ unwrapped = Value.materialize_unwrapped(value)
126
+ return Naming.identity(unwrapped.values).to_s if unwrapped.is_a?(Hash)
127
+
128
+ unwrapped.to_s
129
+ end
130
+
131
+ # A COMMAND ARGUMENT's own related record, reachable by name from
132
+ # `given`/`ensures` — `disputed_by.status`, say, `CardPayment
133
+ # .Dispute`'s own fresh `Reference<Customer>` argument — without
134
+ # teaching the pure expression evaluator anything about
135
+ # repositories. The lookup happens HERE, once, before evaluation;
136
+ # `Resolver#lookup` just digs into a plain Hash exactly as it
137
+ # always has.
138
+ #
139
+ # `owner` NARROWED TO `command` ONLY (S12, ADR 0025 — "rules
140
+ # confined to their own aggregate boundary"): dereferencing the
141
+ # DECLARING aggregate/entity's own STORED `reference_to` used to
142
+ # be the other half of this method's job — a live query against
143
+ # another aggregate's own repository, every time a `given`/
144
+ # `ensures`/`invariant` ran. That half is gone; a cross-aggregate
145
+ # fact a rule needs now has to be a `projects`-maintained LOCAL
146
+ # field (`AggregateBuilder#projects_impl`'s own comment), already
147
+ # present in `subject`'s own state, no hydration needed. A
148
+ # reference-typed COMMAND ARGUMENT stays in bounds, though — the
149
+ # ADR's own boundary list names "its command arguments" as
150
+ # readable, and nothing is stored yet for a fresh argument to
151
+ # project from; resolving it once here, synchronous with THIS
152
+ # command's own admission, is a different shape from a live query
153
+ # against an ALREADY-PERSISTED reference. `enforce_givens`/
154
+ # `enforce_ensures` are this method's only two remaining callers,
155
+ # both passing `command`/`args`, never a `subject`'s own
156
+ # aggregate — verified before this comment was written, not
157
+ # assumed.
158
+ #
159
+ # RECURSES into what it finds, so a chain deeper than one hop
160
+ # still resolves in one pass. Depth-bounded rather than cycle-
161
+ # detected — nothing in this corpus dots more than two hops on a
162
+ # fresh argument, and a bound is simpler than tracking visited
163
+ # (type, id) pairs for a cycle nothing here declares.
164
+ DEREFERENCE_DEPTH = 4
165
+ private_constant :DEREFERENCE_DEPTH
166
+
167
+ def dereference(domain, owner, source, depth: DEREFERENCE_DEPTH)
168
+ return {} if depth <= 0 || owner.nil?
169
+
170
+ owner.attributes.each_with_object({}) do |attribute, hydrated|
171
+ next unless attribute.reference?
172
+
173
+ id = source[attribute.name]
174
+ next if id.nil?
175
+
176
+ target = referenced_aggregate(attribute)
177
+ next unless target
178
+
179
+ record = @registry.repository(domain, target).find(id.to_s)
180
+ next unless record
181
+
182
+ name = attribute.name.to_s.sub(/_id\z/, "").to_sym
183
+ hydrated[name] = record.state.merge(dereference(domain, target, record.state, depth: depth - 1))
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,28 @@
1
+ require_relative "command_rules/admissibility"
2
+ require_relative "command_rules/references"
3
+ require_relative "command_rules/arithmetic"
4
+ require_relative "command_rules/emission"
5
+ require_relative "command_rules/authorization"
6
+
7
+ module Hecks
8
+ module Runtime
9
+ # The rules both interpreters consult, one concern per file beside
10
+ # this one: whether a command may run (admissibility), whether its
11
+ # references point at anything (references), what its arithmetic
12
+ # mutations mean (arithmetic), what its emits become (emission), and
13
+ # whether the caller may run it at all (authorization).
14
+ class CommandRules
15
+ include Admissibility
16
+ include References
17
+ include Arithmetic
18
+ include Emission
19
+ include Authorization
20
+
21
+ attr_reader :registry
22
+
23
+ def initialize(registry)
24
+ @registry = registry
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,245 @@
1
+ require_relative "../bluebook/expression"
2
+
3
+ module Hecks
4
+ module Runtime
5
+ # Persistence-neutral facts derived from a command's existing semantic IR.
6
+ # This module deliberately does not execute a plan or choose a repository.
7
+ module DependencyPlanning
8
+ ATOMIC_PUT = :atomic_put
9
+ TRANSACTIONAL_FALLBACK = :load_apply_validate_store
10
+
11
+ Plan = Struct.new(
12
+ :read_set,
13
+ :write_set,
14
+ :payload_read_set,
15
+ :complete_state,
16
+ :state_independent,
17
+ :unresolved_dependencies,
18
+ keyword_init: true
19
+ ) do
20
+ def complete_state? = complete_state
21
+ def state_independent? = state_independent
22
+
23
+ # Capability negotiation is correctness-first: an optimization is
24
+ # selected only when both the semantic proof and adapter capability
25
+ # are present. This is planning data only; no runtime path calls it yet.
26
+ def strategy_for(capabilities: [])
27
+ return TRANSACTIONAL_FALLBACK unless complete_state? && state_independent?
28
+ return TRANSACTIONAL_FALLBACK unless capabilities.map(&:to_sym).include?(ATOMIC_PUT)
29
+
30
+ ATOMIC_PUT
31
+ end
32
+ end
33
+
34
+ module ExpressionReads
35
+ module_function
36
+
37
+ # Read the same parsed canonical-expression nodes the evaluator uses.
38
+ # A generic Struct walk keeps this additive when the expression grammar
39
+ # gains a composed node; only Lookup nodes carry domain dependencies.
40
+ def paths(canonical)
41
+ collect(Bluebook::Expression::Evaluator.parse(canonical), Set.new)
42
+ end
43
+
44
+ def collect(node, bound_names)
45
+ case node
46
+ when Bluebook::Expression::Resolver::Lookup
47
+ root = node.path.to_s.split(".", 2).first
48
+ bound_names.include?(root) ? [] : [node.path.to_s]
49
+ when Bluebook::Expression::Resolver::BlockPredicate
50
+ collect(node.receiver, bound_names) +
51
+ collect(node.predicate, bound_names | [node.param.to_s])
52
+ when Struct
53
+ node.each_pair.flat_map { |_name, value| collect(value, bound_names) }
54
+ when Array
55
+ node.flat_map { |value| collect(value, bound_names) }
56
+ else
57
+ []
58
+ end
59
+ end
60
+ end
61
+
62
+ class Analyzer
63
+ STATEFUL_MUTATIONS = %i[append increment decrement multiply clamp remove].freeze
64
+
65
+ def self.call(aggregate:, command:) = new(aggregate, command).call
66
+
67
+ def initialize(aggregate, command)
68
+ @aggregate = aggregate
69
+ @command = command
70
+ @owner_fields = aggregate.attributes.to_set(&:name)
71
+ @owner_fields << aggregate.lifecycle.field.to_sym if aggregate.lifecycle
72
+ # `projects` FIELDS (S12, ADR 0025) ARE OWNER STATE TOO — a
73
+ # `given`/`ensures` reading one (e.g. `customer_status ==
74
+ # "active"`) is reading this record's own stored field, same
75
+ # as any attribute, even though nothing here WRITES it via a
76
+ # declared mutation (`CommandInterpreter#seed_projected_fields`
77
+ # populates it outside this analysis entirely). Left out of
78
+ # `known_writes` deliberately: `add_preservation_reads` then
79
+ # correctly treats it as a prior-state read that must survive
80
+ # a partial mutation, which is exactly right — a projected
81
+ # field's freshness comes from the interpreter reseeding it on
82
+ # save, not from anything a caller-supplied write set carries.
83
+ if aggregate.respond_to?(:projected_fields)
84
+ aggregate.projected_fields.each { |field| @owner_fields << field.name }
85
+ end
86
+ @payload_fields = command.attributes.to_set(&:name)
87
+ @state_reads = Set.new
88
+ @payload_reads = Set.new
89
+ @writes = Set.new
90
+ @known_writes = Set.new
91
+ @unresolved = Set.new
92
+ end
93
+
94
+ def call
95
+ analyze_initial_state
96
+ analyze_mutations
97
+ analyze_lifecycle
98
+ analyze_rules(command.givens, phase: :before)
99
+ analyze_rules(command.ensures, phase: :after)
100
+ analyze_rules(aggregate.invariants, phase: :after)
101
+ add_preservation_reads
102
+
103
+ complete = unresolved.empty? && owner_fields.subset?(known_writes)
104
+ independent = complete && state_reads.empty?
105
+
106
+ Plan.new(
107
+ read_set: sorted(state_reads),
108
+ write_set: sorted(writes),
109
+ payload_read_set: sorted(payload_reads),
110
+ complete_state: complete,
111
+ state_independent: independent,
112
+ unresolved_dependencies: unresolved.to_a.sort.freeze
113
+ ).freeze
114
+ end
115
+
116
+ private
117
+
118
+ attr_reader :aggregate, :command, :owner_fields, :payload_fields,
119
+ :state_reads, :payload_reads, :writes, :known_writes, :unresolved
120
+
121
+ # A fresh Instance supplies these values without reading a stored
122
+ # record. Keep this aligned with Instance.defaults/default_for. They
123
+ # establish completeness but are not command mutations, so they do not
124
+ # appear in write_set.
125
+ def analyze_initial_state
126
+ aggregate.attributes.each do |attribute|
127
+ known_writes << attribute.name if deterministic_initial_value?(attribute)
128
+ end
129
+
130
+ known_writes << aggregate.lifecycle.field.to_sym if aggregate.lifecycle
131
+ end
132
+
133
+ def deterministic_initial_value?(attribute)
134
+ return true if attribute.list? || attribute.optional? || !attribute.default.nil?
135
+ return false unless aggregate.respond_to?(:value_object)
136
+
137
+ value_object = aggregate.value_object(attribute.type)
138
+ value_object&.attributes&.all? { |field| !field.default.nil? }
139
+ end
140
+
141
+ def analyze_mutations
142
+ command.mutations.each do |mutation|
143
+ target = mutation.target.to_sym
144
+ writes << target
145
+
146
+ unless owner_fields.include?(target)
147
+ unresolved << "mutation target #{target} is not an aggregate field"
148
+ next
149
+ end
150
+
151
+ if mutation.op == :set
152
+ known_writes << target if analyze_source?(mutation.source)
153
+ elsif STATEFUL_MUTATIONS.include?(mutation.op)
154
+ state_reads << target
155
+ analyze_source?(mutation.source)
156
+ else
157
+ unresolved << "mutation operation #{mutation.op} has no dependency rule"
158
+ end
159
+ end
160
+ end
161
+
162
+ # Returns true only when the source is known without prior aggregate
163
+ # state. Hash sources are the canonical append binding shape.
164
+ def analyze_source?(source)
165
+ case source
166
+ when Symbol
167
+ if payload_fields.include?(source)
168
+ payload_reads << source
169
+ return true
170
+ end
171
+ if owner_fields.include?(source)
172
+ state_reads << source
173
+ return false
174
+ end
175
+
176
+ unresolved << "mutation source #{source} has no payload or aggregate field"
177
+ false
178
+ when Hash
179
+ source.values.map { |value| analyze_source?(value) }.all?
180
+ else
181
+ true
182
+ end
183
+ end
184
+
185
+ def analyze_lifecycle
186
+ lifecycle = aggregate.lifecycle
187
+ return unless lifecycle
188
+
189
+ if command.from
190
+ state_reads << lifecycle.field
191
+ end
192
+
193
+ return if lifecycle.transitions_for(command.hecks_name).empty?
194
+
195
+ writes << lifecycle.field
196
+ known_writes << lifecycle.field
197
+ end
198
+
199
+ def analyze_rules(rules, phase:)
200
+ rules.each do |rule|
201
+ ExpressionReads.paths(rule.canonical).each { |path| classify_path(path, phase) }
202
+ rescue ArgumentError => error
203
+ unresolved << "expression #{rule.canonical.inspect} could not be analyzed: #{error.message}"
204
+ end
205
+ end
206
+
207
+ def classify_path(path, phase)
208
+ head, nested = path.split(".", 2)
209
+ name = head.to_sym
210
+
211
+ if name == :parent
212
+ parent_field = nested.to_s.split(".", 2).first
213
+ if parent_field.empty? || !owner_fields.include?(parent_field.to_sym)
214
+ unresolved << "#{path} does not name parent aggregate state"
215
+ else
216
+ state_reads << parent_field.to_sym
217
+ end
218
+ elsif name == :old
219
+ old_field = nested.to_s.split(".", 2).first
220
+ if old_field.empty? || !owner_fields.include?(old_field.to_sym)
221
+ unresolved << "#{path} does not name prior aggregate state"
222
+ else
223
+ state_reads << old_field.to_sym
224
+ end
225
+ elsif payload_fields.include?(name)
226
+ payload_reads << name
227
+ elsif owner_fields.include?(name)
228
+ state_reads << name if phase == :before || !known_writes.include?(name)
229
+ else
230
+ unresolved << "expression path #{path} has no payload or aggregate field"
231
+ end
232
+ end
233
+
234
+ # A partial mutation must preserve every untouched field on the
235
+ # correctness path. Those prior values are real reads even when no rule
236
+ # names them. A deterministic write needs no preservation read.
237
+ def add_preservation_reads
238
+ state_reads.merge(owner_fields - known_writes)
239
+ end
240
+
241
+ def sorted(values) = values.to_a.sort.freeze
242
+ end
243
+ end
244
+ end
245
+ end