hecks 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (408) hide show
  1. checksums.yaml +5 -5
  2. data/lib/hecks/adapters/driven/claude_code.adapter +3 -0
  3. data/lib/hecks/adapters/driven/claude_code.rb +127 -0
  4. data/lib/hecks/adapters/driven/d1.adapter +12 -0
  5. data/lib/hecks/adapters/driven/d1.rb +403 -0
  6. data/lib/hecks/adapters/driven/folder.adapter +3 -0
  7. data/lib/hecks/adapters/driven/folder.rb +199 -0
  8. data/lib/hecks/adapters/driven/google_authentication.adapter +3 -0
  9. data/lib/hecks/adapters/driven/google_authentication.rb +100 -0
  10. data/lib/hecks/adapters/driven/governance_authorization.adapter +3 -0
  11. data/lib/hecks/adapters/driven/governance_authorization.rb +62 -0
  12. data/lib/hecks/adapters/driven/heki/journal.rb +51 -0
  13. data/lib/hecks/adapters/driven/heki/saga_store.rb +0 -0
  14. data/lib/hecks/adapters/driven/heki/snapshot.rb +38 -0
  15. data/lib/hecks/adapters/driven/heki.adapter +4 -0
  16. data/lib/hecks/adapters/driven/heki.rb +148 -0
  17. data/lib/hecks/adapters/driven/identity_registry.adapter +3 -0
  18. data/lib/hecks/adapters/driven/identity_registry.rb +26 -0
  19. data/lib/hecks/adapters/driven/in_memory_ordering.rb +51 -0
  20. data/lib/hecks/adapters/driven/lambda/client.rb +63 -0
  21. data/lib/hecks/adapters/driven/lambda.adapter +4 -0
  22. data/lib/hecks/adapters/driven/lambda.rb +131 -0
  23. data/lib/hecks/adapters/driven/memory.adapter +3 -0
  24. data/lib/hecks/adapters/driven/memory.rb +103 -0
  25. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +3 -0
  26. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +28 -0
  27. data/lib/hecks/adapters/driven/postgres/codec.rb +80 -0
  28. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +168 -0
  29. data/lib/hecks/adapters/driven/postgres.adapter +5 -0
  30. data/lib/hecks/adapters/driven/postgres.rb +391 -0
  31. data/lib/hecks/adapters/driven/postgres_era/lineage/era_store.rb +171 -0
  32. data/lib/hecks/adapters/driven/postgres_era/lineage/field_cache.rb +190 -0
  33. data/lib/hecks/adapters/driven/postgres_era/lineage/head_compiler.rb +429 -0
  34. data/lib/hecks/adapters/driven/postgres_era/lineage/mint_transaction.rb +166 -0
  35. data/lib/hecks/adapters/driven/postgres_era/lineage/provisioning.rb +286 -0
  36. data/lib/hecks/adapters/driven/postgres_era/lineage/resumable_backfill.rb +168 -0
  37. data/lib/hecks/adapters/driven/postgres_era/lineage/tail_merge.rb +163 -0
  38. data/lib/hecks/adapters/driven/postgres_era/lineage/transform_installer.rb +114 -0
  39. data/lib/hecks/adapters/driven/postgres_era/lineage.rb +137 -0
  40. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/coverage_check.rb +89 -0
  41. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/era_resolver.rb +82 -0
  42. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
  43. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/minter.rb +132 -0
  44. data/lib/hecks/adapters/driven/postgres_era/lineage_manager.rb +74 -0
  45. data/lib/hecks/adapters/driven/postgres_era.adapter +6 -0
  46. data/lib/hecks/adapters/driven/postgres_era.rb +671 -0
  47. data/lib/hecks/adapters/driven/prism.adapter +3 -0
  48. data/lib/hecks/adapters/driven/prism.rb +80 -0
  49. data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
  50. data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
  51. data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
  52. data/lib/hecks/adapters/driven/sqlite/codec.rb +63 -0
  53. data/lib/hecks/adapters/driven/sqlite/projection.rb +105 -0
  54. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +180 -0
  55. data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
  56. data/lib/hecks/adapters/driven/sqlite.rb +285 -0
  57. data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
  58. data/lib/hecks/adapters/driven/system_clock.rb +14 -0
  59. data/lib/hecks/adapters/driven.rb +36 -0
  60. data/lib/hecks/adapters.rb +6 -0
  61. data/lib/hecks/behaviors/dsl.rb +96 -0
  62. data/lib/hecks/behaviors/expectations.rb +280 -0
  63. data/lib/hecks/behaviors/ir.rb +31 -0
  64. data/lib/hecks/behaviors/rspec.rb +42 -0
  65. data/lib/hecks/behaviors/runner.rb +96 -0
  66. data/lib/hecks/behaviors.rb +25 -0
  67. data/lib/hecks/bluebook/aggregate.rb +108 -0
  68. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
  69. data/lib/hecks/bluebook/assembly/build.rb +48 -0
  70. data/lib/hecks/bluebook/assembly/contract.rb +112 -0
  71. data/lib/hecks/bluebook/assembly/contracts.rb +409 -0
  72. data/lib/hecks/bluebook/assembly/marks.rb +227 -0
  73. data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
  74. data/lib/hecks/bluebook/assembly.rb +78 -0
  75. data/lib/hecks/bluebook/attribute.rb +96 -0
  76. data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
  77. data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
  78. data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
  79. data/lib/hecks/bluebook/behaviour/command.rb +116 -0
  80. data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
  81. data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
  82. data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
  83. data/lib/hecks/bluebook/behaviour/lifecycle.rb +51 -0
  84. data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
  85. data/lib/hecks/bluebook/behaviour/process_manager.rb +38 -0
  86. data/lib/hecks/bluebook/behaviour/query.rb +10 -0
  87. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
  88. data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
  89. data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
  90. data/lib/hecks/bluebook/chapter.rb +68 -0
  91. data/lib/hecks/bluebook/command.rb +124 -0
  92. data/lib/hecks/bluebook/domain_port.rb +102 -0
  93. data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
  94. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +906 -0
  95. data/lib/hecks/bluebook/dsl/attribute_collector.rb +327 -0
  96. data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
  97. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1018 -0
  98. data/lib/hecks/bluebook/dsl/command_builder.rb +626 -0
  99. data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
  100. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
  101. data/lib/hecks/bluebook/dsl/entity_builder.rb +305 -0
  102. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
  103. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
  104. data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
  105. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
  106. data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
  107. data/lib/hecks/bluebook/dsl/policy_builder.rb +120 -0
  108. data/lib/hecks/bluebook/dsl/port_builder.rb +30 -0
  109. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
  110. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +207 -0
  111. data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
  112. data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
  113. data/lib/hecks/bluebook/dsl/rule_reference.rb +172 -0
  114. data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
  115. data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
  116. data/lib/hecks/bluebook/dsl/word_gate.rb +221 -0
  117. data/lib/hecks/bluebook/dsl/world_builder.rb +77 -0
  118. data/lib/hecks/bluebook/dsl.rb +45 -0
  119. data/lib/hecks/bluebook/entity.rb +103 -0
  120. data/lib/hecks/bluebook/expression/canonical_form.rb +55 -0
  121. data/lib/hecks/bluebook/expression/evaluator.rb +261 -0
  122. data/lib/hecks/bluebook/expression/projection.json +170 -0
  123. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
  124. data/lib/hecks/bluebook/expression/resolver.rb +630 -0
  125. data/lib/hecks/bluebook/expression.rb +13 -0
  126. data/lib/hecks/bluebook/hexagon.rb +60 -0
  127. data/lib/hecks/bluebook/lifecycle.rb +42 -0
  128. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
  129. data/lib/hecks/bluebook/meta_validator/judge.rb +559 -0
  130. data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
  131. data/lib/hecks/bluebook/meta_validator/port_judge.rb +47 -0
  132. data/lib/hecks/bluebook/meta_validator/readings.rb +350 -0
  133. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +312 -0
  134. data/lib/hecks/bluebook/meta_validator/shapes.rb +246 -0
  135. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
  136. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
  137. data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
  138. data/lib/hecks/bluebook/meta_validator.rb +452 -0
  139. data/lib/hecks/bluebook/model_check.rb +318 -0
  140. data/lib/hecks/bluebook/pattern_subset.rb +120 -0
  141. data/lib/hecks/bluebook/policy.rb +41 -0
  142. data/lib/hecks/bluebook/process_manager.rb +86 -0
  143. data/lib/hecks/bluebook/project_discovery.rb +30 -0
  144. data/lib/hecks/bluebook/project_loader.rb +40 -0
  145. data/lib/hecks/bluebook/project_register.rb +82 -0
  146. data/lib/hecks/bluebook/query.rb +61 -0
  147. data/lib/hecks/bluebook/read_model.rb +109 -0
  148. data/lib/hecks/bluebook/reference.rb +74 -0
  149. data/lib/hecks/bluebook/smoke_test.rb +166 -0
  150. data/lib/hecks/bluebook/synthesizer.rb +95 -0
  151. data/lib/hecks/bluebook/translation.rb +92 -0
  152. data/lib/hecks/bluebook/value_object.rb +58 -0
  153. data/lib/hecks/bluebook.rb +74 -0
  154. data/lib/hecks/codemod.rb +342 -0
  155. data/lib/hecks/construct.rb +71 -0
  156. data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
  157. data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
  158. data/lib/hecks/deploy/oidc.json +18 -0
  159. data/lib/hecks/doc/reference.rb +389 -0
  160. data/lib/hecks/embryonaut_bluebook.rb +75 -0
  161. data/lib/hecks/facade/cli_door.rb +116 -0
  162. data/lib/hecks/facade/cli_runner.rb +190 -0
  163. data/lib/hecks/facade/command_request.rb +105 -0
  164. data/lib/hecks/facade/handle.rb +173 -0
  165. data/lib/hecks/facade/json_door.rb +154 -0
  166. data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
  167. data/lib/hecks/facade/surface/chapter.rb +107 -0
  168. data/lib/hecks/facade/surface.rb +48 -0
  169. data/lib/hecks/facade.rb +44 -0
  170. data/lib/hecks/forms/app.rb +300 -0
  171. data/lib/hecks/forms/command_form_renderer.rb +113 -0
  172. data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
  173. data/lib/hecks/forms/field_renderer.rb +170 -0
  174. data/lib/hecks/forms/field_shape.rb +232 -0
  175. data/lib/hecks/forms/html.rb +53 -0
  176. data/lib/hecks/forms/index_renderer.rb +35 -0
  177. data/lib/hecks/forms/page.rb +157 -0
  178. data/lib/hecks/forms/params.rb +132 -0
  179. data/lib/hecks/forms/query_form_renderer.rb +114 -0
  180. data/lib/hecks/forms/record_renderer.rb +115 -0
  181. data/lib/hecks/forms/record_table.rb +63 -0
  182. data/lib/hecks/forms/reference_options.rb +30 -0
  183. data/lib/hecks/forms/value_object_shape.rb +46 -0
  184. data/lib/hecks/forms.rb +54 -0
  185. data/lib/hecks/fqn.rb +94 -0
  186. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
  187. data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
  188. data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
  189. data/lib/hecks/framework/bluebook/governance.bluebook +130 -0
  190. data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
  191. data/lib/hecks/framework/oidc.json +39 -0
  192. data/lib/hecks/framework.rb +90 -0
  193. data/lib/hecks/freezer.rb +67 -0
  194. data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
  195. data/lib/hecks/fuzzing/isolated_boot.rb +92 -0
  196. data/lib/hecks/fuzzing/properties.rb +1146 -0
  197. data/lib/hecks/fuzzing/replay.rb +635 -0
  198. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
  199. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +54 -0
  200. data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
  201. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +185 -0
  202. data/lib/hecks/fuzzing/sequence_generator.rb +130 -0
  203. data/lib/hecks/fuzzing/value_generator.rb +177 -0
  204. data/lib/hecks/fuzzing.rb +12 -0
  205. data/lib/hecks/grammar/evolve.rb +291 -0
  206. data/lib/hecks/grammar/expression.bluebook +417 -0
  207. data/lib/hecks/grammar/expression_operators.json +1152 -0
  208. data/lib/hecks/grammar/grammar.hecksagon +20 -0
  209. data/lib/hecks/grammar/oidc.json +69 -0
  210. data/lib/hecks/grammar/translation.bluebook +203 -0
  211. data/lib/hecks/grammar.rb +178 -0
  212. data/lib/hecks/ir.rb +126 -0
  213. data/lib/hecks/language/adapter.bluebook +116 -0
  214. data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
  215. data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
  216. data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
  217. data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
  218. data/lib/hecks/language/bluebook/command.bluebook +437 -0
  219. data/lib/hecks/language/bluebook/entity.bluebook +316 -0
  220. data/lib/hecks/language/bluebook/policy.bluebook +172 -0
  221. data/lib/hecks/language/bluebook/process_manager.bluebook +264 -0
  222. data/lib/hecks/language/bluebook/projection.bluebook +267 -0
  223. data/lib/hecks/language/bluebook/query.bluebook +245 -0
  224. data/lib/hecks/language/bluebook/shape.bluebook +292 -0
  225. data/lib/hecks/language/bluebook/syntax.bluebook +447 -0
  226. data/lib/hecks/language/bluebook/vocabulary.bluebook +364 -0
  227. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
  228. data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
  229. data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
  230. data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
  231. data/lib/hecks/language/oidc.json +313 -0
  232. data/lib/hecks/language/port.bluebook +92 -0
  233. data/lib/hecks/language/translation/translation.bluebook +110 -0
  234. data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
  235. data/lib/hecks/language/world/wiring.bluebook +62 -0
  236. data/lib/hecks/language/world/world.bluebook +84 -0
  237. data/lib/hecks/literal.rb +125 -0
  238. data/lib/hecks/naming.rb +121 -0
  239. data/lib/hecks/ports/access_control.port +4 -0
  240. data/lib/hecks/ports/access_control.rb +62 -0
  241. data/lib/hecks/ports/agent/answers.rb +104 -0
  242. data/lib/hecks/ports/agent.port +4 -0
  243. data/lib/hecks/ports/agent.rb +167 -0
  244. data/lib/hecks/ports/authentication.port +4 -0
  245. data/lib/hecks/ports/authentication.rb +50 -0
  246. data/lib/hecks/ports/authorization.port +4 -0
  247. data/lib/hecks/ports/authorization.rb +59 -0
  248. data/lib/hecks/ports/clock.port +4 -0
  249. data/lib/hecks/ports/clock.rb +62 -0
  250. data/lib/hecks/ports/extraction.port +4 -0
  251. data/lib/hecks/ports/extraction.rb +37 -0
  252. data/lib/hecks/ports/identity_assignment.port +4 -0
  253. data/lib/hecks/ports/identity_assignment.rb +45 -0
  254. data/lib/hecks/ports/identity_generation.port +4 -0
  255. data/lib/hecks/ports/identity_generation.rb +49 -0
  256. data/lib/hecks/ports/identity_resolution.port +4 -0
  257. data/lib/hecks/ports/identity_resolution.rb +40 -0
  258. data/lib/hecks/ports/loading.port +4 -0
  259. data/lib/hecks/ports/loading.rb +13 -0
  260. data/lib/hecks/ports/persistence/append_only.rb +102 -0
  261. data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
  262. data/lib/hecks/ports/persistence/execution.rb +18 -0
  263. data/lib/hecks/ports/persistence/lineage.rb +277 -0
  264. data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
  265. data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
  266. data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
  267. data/lib/hecks/ports/persistence.port +4 -0
  268. data/lib/hecks/ports/persistence.rb +39 -0
  269. data/lib/hecks/ports/projection.port +4 -0
  270. data/lib/hecks/ports/projection.rb +76 -0
  271. data/lib/hecks/ports/query/in_memory.rb +60 -0
  272. data/lib/hecks/ports/query/ordering.rb +41 -0
  273. data/lib/hecks/ports/query.rb +36 -0
  274. data/lib/hecks/ports.rb +27 -0
  275. data/lib/hecks/projections/diagrams.rb +413 -0
  276. data/lib/hecks/projections/ir.rb +18 -0
  277. data/lib/hecks/projections/model/deviations.rb +98 -0
  278. data/lib/hecks/projections/model.rb +145 -0
  279. data/lib/hecks/projections/oidc.rb +110 -0
  280. data/lib/hecks/projections/parser_table.rb +159 -0
  281. data/lib/hecks/projections/reference.rb +38 -0
  282. data/lib/hecks/projections/shape.rb +31 -0
  283. data/lib/hecks/projections/statements.rb +110 -0
  284. data/lib/hecks/projections/vocabulary.rb +100 -0
  285. data/lib/hecks/projections.rb +32 -0
  286. data/lib/hecks/projector/cli_projector.rb +437 -0
  287. data/lib/hecks/projector/docs_projector.rb +321 -0
  288. data/lib/hecks/projector/exporter.rb +141 -0
  289. data/lib/hecks/projector/ir_projector.rb +18 -0
  290. data/lib/hecks/projector/narrate_projector.rb +243 -0
  291. data/lib/hecks/projector/target.rb +97 -0
  292. data/lib/hecks/projector.rb +186 -0
  293. data/lib/hecks/query_ir.rb +392 -0
  294. data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
  295. data/lib/hecks/query_specification/common/comparators.rb +25 -0
  296. data/lib/hecks/query_specification/common/comparison.rb +174 -0
  297. data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
  298. data/lib/hecks/query_specification/common/dsl.rb +58 -0
  299. data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
  300. data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
  301. data/lib/hecks/query_specification/common/null_policy.rb +80 -0
  302. data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
  303. data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
  304. data/lib/hecks/query_specification/common/options.rb +32 -0
  305. data/lib/hecks/query_specification/common/order_by.rb +9 -0
  306. data/lib/hecks/query_specification/common/specification.rb +10 -0
  307. data/lib/hecks/query_specification/common/where_clause.rb +9 -0
  308. data/lib/hecks/query_specification/field_path.rb +89 -0
  309. data/lib/hecks/query_specification/hop_path.rb +130 -0
  310. data/lib/hecks/query_specification/read_model/specification.rb +18 -0
  311. data/lib/hecks/query_specification.rb +14 -0
  312. data/lib/hecks/rendering.rb +48 -0
  313. data/lib/hecks/router/namespace_installer.rb +157 -0
  314. data/lib/hecks/router.rb +70 -0
  315. data/lib/hecks/runtime/caller.rb +50 -0
  316. data/lib/hecks/runtime/capability_graph.rb +44 -0
  317. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +124 -0
  318. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +248 -0
  319. data/lib/hecks/runtime/command_interpreter.rb +413 -0
  320. data/lib/hecks/runtime/command_rules/admissibility.rb +307 -0
  321. data/lib/hecks/runtime/command_rules/arithmetic.rb +254 -0
  322. data/lib/hecks/runtime/command_rules/authorization.rb +64 -0
  323. data/lib/hecks/runtime/command_rules/emission.rb +34 -0
  324. data/lib/hecks/runtime/command_rules/references.rb +181 -0
  325. data/lib/hecks/runtime/command_rules.rb +28 -0
  326. data/lib/hecks/runtime/dependency_planning.rb +231 -0
  327. data/lib/hecks/runtime/dispatcher.rb +286 -0
  328. data/lib/hecks/runtime/entity_element.rb +253 -0
  329. data/lib/hecks/runtime/entity_interpreter.rb +228 -0
  330. data/lib/hecks/runtime/era_check.rb +136 -0
  331. data/lib/hecks/runtime/era_guard/shape_diff.rb +128 -0
  332. data/lib/hecks/runtime/era_guard.rb +193 -0
  333. data/lib/hecks/runtime/era_tamper.rb +61 -0
  334. data/lib/hecks/runtime/errors.rb +100 -0
  335. data/lib/hecks/runtime/event.rb +51 -0
  336. data/lib/hecks/runtime/identity.rb +128 -0
  337. data/lib/hecks/runtime/instance.rb +120 -0
  338. data/lib/hecks/runtime/interpreting.rb +69 -0
  339. data/lib/hecks/runtime/loader.rb +143 -0
  340. data/lib/hecks/runtime/policy_interpreter.rb +366 -0
  341. data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
  342. data/lib/hecks/runtime/query_interpreter.rb +254 -0
  343. data/lib/hecks/runtime/reaction_invocation.rb +245 -0
  344. data/lib/hecks/runtime/read_model_interpreter.rb +266 -0
  345. data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
  346. data/lib/hecks/runtime/reference_hop.rb +99 -0
  347. data/lib/hecks/runtime/refusal_wording.rb +124 -0
  348. data/lib/hecks/runtime/registry/saga_persistence.rb +70 -0
  349. data/lib/hecks/runtime/registry/verification.rb +158 -0
  350. data/lib/hecks/runtime/registry.rb +218 -0
  351. data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
  352. data/lib/hecks/runtime/routing.rb +96 -0
  353. data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
  354. data/lib/hecks/runtime/saga_interpreter.rb +301 -0
  355. data/lib/hecks/runtime/storage_shape.rb +120 -0
  356. data/lib/hecks/runtime/tenant_check.rb +84 -0
  357. data/lib/hecks/runtime/tenant_scope.rb +55 -0
  358. data/lib/hecks/runtime/value/admission.rb +112 -0
  359. data/lib/hecks/runtime/value/coercion.rb +500 -0
  360. data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
  361. data/lib/hecks/runtime/value.rb +125 -0
  362. data/lib/hecks/runtime.rb +95 -0
  363. data/lib/hecks/translation/audit/approval_digest.rb +31 -0
  364. data/lib/hecks/translation/audit/layer_one.rb +37 -0
  365. data/lib/hecks/translation/audit/layer_two.rb +77 -0
  366. data/lib/hecks/translation/audit/unfed_report.rb +42 -0
  367. data/lib/hecks/translation/audit.rb +70 -0
  368. data/lib/hecks/translation/reattest.rb +72 -0
  369. data/lib/hecks/translation/rule_compiler.rb +120 -0
  370. data/lib/hecks/translation/scaffold/differ.rb +183 -0
  371. data/lib/hecks/translation/scaffold/renderer.rb +41 -0
  372. data/lib/hecks/translation/scaffold/writer.rb +28 -0
  373. data/lib/hecks/translation/scaffold.rb +30 -0
  374. data/lib/hecks/translation.rb +11 -0
  375. data/lib/hecks/version.rb +16 -0
  376. data/lib/hecks/vocabulary.rb +213 -0
  377. data/lib/hecks.rb +118 -6
  378. metadata +401 -222
  379. data/bin/hecks +0 -7
  380. data/bin/hecks-package +0 -65
  381. data/bin/hecks_console +0 -12
  382. data/bin/hecks_serverless +0 -6
  383. data/lib/cli/build.rb +0 -14
  384. data/lib/cli/command_runner.rb +0 -28
  385. data/lib/cli/console.rb +0 -10
  386. data/lib/cli/generate.rb +0 -37
  387. data/lib/cli/hecks-cli.rb +0 -27
  388. data/lib/cli/test.rb +0 -57
  389. data/lib/console/commands.rb +0 -8
  390. data/lib/console/hecks-console.rb +0 -1
  391. data/lib/packager/README.md +0 -0
  392. data/lib/packager/app_runner.rb +0 -21
  393. data/lib/packager/args.rb +0 -26
  394. data/lib/packager/compatibility/fixnum.rb +0 -6
  395. data/lib/packager/hecks.rb +0 -39
  396. data/lib/packager/query_runner.rb +0 -21
  397. data/lib/packager/resources/Dockerfile +0 -11
  398. data/lib/packager/resources/app_binary +0 -7
  399. data/lib/packager/resources/bundle_config +0 -3
  400. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
  401. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
  402. data/lib/serverless/Domain +0 -32
  403. data/lib/serverless/cli.rb +0 -75
  404. data/lib/serverless/resources/command_name.js +0 -5
  405. data/lib/serverless/resources/environment.js +0 -7
  406. data/lib/serverless/resources/handler.js.tt +0 -28
  407. data/lib/serverless/resources/run_binary.js +0 -22
  408. data/lib/serverless/resources/serverless.yml +0 -20
@@ -0,0 +1,228 @@
1
+ require_relative "interpreting"
2
+ require_relative "../naming"
3
+ require_relative "../rendering"
4
+ require_relative "errors"
5
+ require_relative "identity"
6
+ require_relative "instance"
7
+ require_relative "value"
8
+ require_relative "refusal_wording"
9
+ require_relative "routing"
10
+ require_relative "dependency_planning"
11
+ require_relative "../ports/persistence/execution"
12
+ require_relative "entity_element"
13
+
14
+ module Hecks
15
+ module Runtime
16
+ class EntityInterpreter
17
+ include Interpreting
18
+
19
+ attr_reader :registry
20
+
21
+ # THE DECLARED ORDER, HAND-TYPED — mirrors Vocabulary::EntityDispatchOrder
22
+ # (language/bluebook/vocabulary.bluebook:217-232), held equal to it by
23
+ # spec/vocabulary_conformance_spec.rb the same way CommandInterpreter's
24
+ # own DISPATCH_ORDER is; see that constant's doc comment for why this is
25
+ # hand-typed rather than read live off the meta-domain at every dispatch.
26
+ # Shorter than the aggregate order for the same reasons the declaration
27
+ # itself gives : no refuse_unknown_arguments/refuse_absent_arguments (an
28
+ # entity inherits its aggregate's own gate) and no
29
+ # assign_creation_attributes (an entity is never created through this
30
+ # path).
31
+ DISPATCH_ORDER = Hecks::Vocabulary.symbols("EntityDispatchOrder")
32
+
33
+ # `instance` is the PARENT aggregate record (what gets saved and
34
+ # returned) ; `element`/`view` are the entity piece itself — `view`
35
+ # wraps `element` as it stood at `locate_element`, pre-mutation, and
36
+ # `enforce_ensures` builds its own settled wrapper off `element` as it
37
+ # stands after, the same split the original sequential code made.
38
+ #
39
+ # `chain` — S17, ADR 0026 — every entity the dotted verb passes
40
+ # through, root-first (`[Handler, Dispatch]` for `Handler.Dispatch.
41
+ # Bind`) ; `entity`/`entity_name` stay the CHAIN'S OWN LAST entry,
42
+ # the one a command actually belongs to and a mutation actually
43
+ # targets, so every step written before this ADR (enforce_givens,
44
+ # apply_mutations, advance_lifecycle, element_identity, ...) reads
45
+ # exactly as it always has. Only `locate_element` walks the chain.
46
+ Context = Struct.new(:domain, :aggregate, :entity, :entity_name, :command, :command_name,
47
+ :args, :repository, :instance, :chain, :element, :view, :transition,
48
+ :old_element, :result, :route, :plan, :persistence_outcome, :dry_run)
49
+
50
+ def initialize(registry, rules:)
51
+ @registry = registry
52
+ @rules = rules
53
+ end
54
+
55
+ # `dry_run:` — CommandInterpreter#call's own twin, see that method's
56
+ # comment for the shared reasoning (Dispatcher#dry_run?'s own entry
57
+ # point). `step_save`/`step_emit` are the only two steps here that
58
+ # read it either.
59
+ def call(domain, aggregate, dotted, legacy_args, route: nil, with: nil, dry_run: false)
60
+ *entity_names, command_name = dotted.to_s.split(".")
61
+ if entity_names.empty?
62
+ raise UnknownVerb, RefusalWording.render("UnknownVerb", "entity_unknown",
63
+ aggregate: aggregate.hecks_name, entity: dotted.to_s.inspect)
64
+ end
65
+
66
+ chain = walk_entity_chain(aggregate, entity_names)
67
+ entity = chain.last
68
+ command = entity.command(command_name) ||
69
+ raise(UnknownVerb, RefusalWording.render("UnknownVerb", "entity_no_command",
70
+ entity: entity.hecks_name, command: command_name.inspect))
71
+
72
+ args = Routing.payload(command, with: with, legacy: legacy_args)
73
+ ctx = Context.new(domain, aggregate, entity, entity_names.join("."), command, command_name, args)
74
+ ctx.chain = chain
75
+ ctx.route = route
76
+ ctx.dry_run = dry_run
77
+ ctx.plan = DependencyPlanning::Analyzer.call(aggregate: entity, command: command)
78
+ run_dispatch_order(DISPATCH_ORDER, ctx)
79
+ [ctx.instance, ctx.result, ctx.plan, ctx.persistence_outcome]
80
+ end
81
+
82
+ private
83
+
84
+ # ONE HOP PER DOTTED SEGMENT — `ProcessManager.Handler.Dispatch.Bind`
85
+ # (once the dispatcher has already stripped "Domain::Aggregate.")
86
+ # walks Handler off the aggregate, then Dispatch off Handler, each
87
+ # step reading `.entities` exactly the way the single-level case
88
+ # always did — a nested entity is "structurally interchangeable
89
+ # with an aggregate" (Entity's own header) for precisely this
90
+ # reason. Two levels is what Handler/Dispatch need today ; nothing
91
+ # here assumes it stops at two.
92
+ def walk_entity_chain(aggregate, entity_names)
93
+ owner = aggregate
94
+ entity_names.map do |name|
95
+ found = owner.entities.find { |piece| piece.hecks_name == name } ||
96
+ raise(UnknownVerb, RefusalWording.render("UnknownVerb", "entity_unknown",
97
+ aggregate: owner.hecks_name, entity: name.inspect))
98
+ owner = found
99
+ found
100
+ end
101
+ end
102
+
103
+ def step_normalize_args(ctx)
104
+ ctx.args = step(:normalize_args) { normalize_args(ctx.aggregate, ctx.command, ctx.args) }
105
+ end
106
+
107
+ def step_refuse_role_mismatch(ctx)
108
+ step(:refuse_role_mismatch) { @rules.refuse_role_mismatch(ctx.command, ctx.domain) }
109
+ end
110
+
111
+ def step_resolve_references(ctx)
112
+ step(:resolve_references) { @rules.resolve_references(ctx.domain, ctx.command, ctx.args) }
113
+ end
114
+
115
+ def step_hydrate_parent(ctx)
116
+ ctx.repository = @registry.repository(ctx.domain, ctx.aggregate)
117
+ ctx.instance = step(:hydrate_parent) {
118
+ parent(ctx.repository, ctx.aggregate, ctx.entity_name, ctx.command_name, ctx.args, ctx.route)
119
+ }
120
+ end
121
+
122
+ def step_locate_element(ctx)
123
+ ctx.element = step(:locate_element) {
124
+ EntityElement.locate_chain(ctx.aggregate, ctx.chain, ctx.instance, ctx.args, ctx.command_name, ctx.route)
125
+ }
126
+ # `view` was hydrated ONCE, here, into its OWN state hash
127
+ # (Value.hydrate builds a fresh Hash — never aliased with `element`)
128
+ # — exactly right for enforce_givens, which must read pre-mutation.
129
+ ctx.view = Instance.new(aggregate: ctx.entity, id: EntityElement.element_identity(ctx.entity, ctx.element).to_s,
130
+ state: ctx.element)
131
+ end
132
+
133
+ def step_enforce_givens(ctx)
134
+ step(:enforce_givens) {
135
+ @rules.enforce_givens(ctx.view, ctx.command, ctx.args, domain: ctx.domain, declaring: ctx.entity, parent: ctx.instance)
136
+ }
137
+ end
138
+
139
+ def step_admissible_transition(ctx)
140
+ ctx.transition = step(:admissible_transition) { @rules.admissible_transition(ctx.entity, ctx.command, ctx.view) }
141
+ end
142
+
143
+ def step_apply_mutations(ctx)
144
+ ctx.old_element = ctx.element.dup unless ctx.command.ensures.empty?
145
+ step(:apply_mutations) {
146
+ ctx.command.mutations.each { |mutation|
147
+ EntityElement.apply_to_element(@rules, ctx.aggregate, ctx.entity, ctx.element, mutation, ctx.args)
148
+ }
149
+ }
150
+ end
151
+
152
+ def step_advance_lifecycle(ctx)
153
+ return unless ctx.transition
154
+
155
+ step(:advance_lifecycle) { ctx.element[ctx.entity.lifecycle.field] = ctx.transition.target }
156
+ end
157
+
158
+ # An ensures reads the SETTLED record, so it needs a view hydrated from
159
+ # `element` as it stands now, mutations included — unlike `view` above,
160
+ # built once and read pre-mutation by enforce_givens.
161
+ def step_enforce_ensures(ctx)
162
+ step(:enforce_ensures) do
163
+ settled = Instance.new(aggregate: ctx.entity, id: ctx.view.id, state: ctx.element)
164
+ @rules.enforce_ensures(settled, ctx.command, ctx.args, old: ctx.old_element, domain: ctx.domain, parent: ctx.instance)
165
+ end
166
+ end
167
+
168
+ # THE PARENT AGGREGATE's own invariants — `ctx.instance` is the
169
+ # parent record an entity mutation writes into (this file's own
170
+ # `Context` comment), the SAME boundary an aggregate-level
171
+ # invariant guards regardless of which interpreter changed it. No
172
+ # separate "entity invariant" exists (S10, ADR 0025 scopes
173
+ # `invariant` to the aggregate only) — see `Admissibility#
174
+ # enforce_invariants`'s own comment.
175
+ def step_enforce_invariants(ctx)
176
+ step(:enforce_invariants) { @rules.enforce_invariants(ctx.instance, ctx.aggregate, domain: ctx.domain) }
177
+ end
178
+
179
+ # `dry_run:` skips this — see CommandInterpreter#step_save's own
180
+ # comment, same reasoning and the same precedent
181
+ # (`step_assign_creation_attributes`'s own conditional-skip).
182
+ def step_save(ctx)
183
+ return if ctx.dry_run
184
+
185
+ step(:save) do
186
+ @rules.resolve_state_references(ctx.domain, ctx.aggregate, ctx.instance.state)
187
+ ctx.repository.save(ctx.instance)
188
+ ctx.persistence_outcome = Ports::Persistence::Outcome.new(status: :saved, instance: ctx.instance)
189
+ end
190
+ end
191
+
192
+ # `dry_run:` skips this too — nothing was committed, so `ctx.result`
193
+ # stays nil and `Dispatcher#dry_run?` never reads it.
194
+ def step_emit(ctx)
195
+ return if ctx.dry_run
196
+
197
+ ctx.result = step(:emit) { @rules.emit(ctx.command, ctx.domain, ctx.aggregate, ctx.instance, ctx.args, ctx.repository) }
198
+ end
199
+
200
+ # THE PARENT AGGREGATE, addressed exactly as `CommandInterpreter#hydrate`
201
+ # addresses one acting on itself — derive from the declared identity first
202
+ # (`Identity.of`), and let a bare `id:` name an already-derived record when
203
+ # the identity itself is not what the caller is holding.
204
+ def parent(repository, aggregate, entity_name, command_name, args, route = nil)
205
+ parent_id = route&.aggregate ||
206
+ Identity.of(aggregate, args) ||
207
+ Identity.from(aggregate, args, :id) ||
208
+ raise(NotFound, RefusalWording.render("NotFound", "entity_parent_no_identity",
209
+ command: command_name, aggregate: aggregate.hecks_name,
210
+ entity: entity_name, identity: Identity.reading(aggregate)))
211
+ found = repository.find(parent_id) ||
212
+ raise(NotFound, RefusalWording.render("NotFound", "record_missing",
213
+ aggregate: aggregate.hecks_name,
214
+ identity: Identity.reading(aggregate),
215
+ offered: Rendering.describe(parent_id)))
216
+ found.dup
217
+ end
218
+
219
+ # `locate_chain`/`element_of`/`element_identity`/`apply_to_element` and
220
+ # their own helpers used to live here — moved to `Runtime::EntityElement`
221
+ # (see that file's own header) so `CommandInterpreter`'s own
222
+ # `delegate_to_entity` step can locate and mutate the same element the
223
+ # same way, against an aggregate record already held in memory. `call`,
224
+ # above, and every `step_*` method reach them through that module now;
225
+ # nothing about the STEPS themselves changed.
226
+ end
227
+ end
228
+ end
@@ -0,0 +1,136 @@
1
+ require_relative "../ports/persistence"
2
+ require_relative "../ports/persistence/binding_policy"
3
+ require_relative "../ports/persistence/lineage"
4
+ require_relative "../naming"
5
+ require_relative "../framework"
6
+ require_relative "registry"
7
+
8
+ module Hecks
9
+ module Runtime
10
+ # The boot-time era gate, run for the adapters that HAVE eras — the
11
+ # lineage-capable ones. An era is a fact about stored data that some
12
+ # adapter can carry across a shape change; an adapter that cannot
13
+ # translate has no era to hold, and holding one for it would record
14
+ # history about data nothing can act on.
15
+ #
16
+ # The whole domain is handed to the capable adapter's own era check
17
+ # (Postgres: hold, recognize, mint, or refuse toward the scaffold),
18
+ # which keeps its era facts as rows BESIDE its data. That
19
+ # co-location is load-bearing — a watermark only means something
20
+ # against the journal it was cut from, and an approval recorded
21
+ # anywhere but the reviewed database binds to nothing.
22
+ #
23
+ # The capability is asked of the adapter, never of its name: a
24
+ # second adapter that grows an era story declares lineage_capable?
25
+ # and era_check! and needs no change here.
26
+ #
27
+ # Detection and identity are separate jobs: this check never hashes
28
+ # anything — era names come from the store, already minted (by the
29
+ # scaffold) or absent. Refusal wordings are contract, pinned by the
30
+ # corpus.
31
+ module EraCheck
32
+ module_function
33
+
34
+ # EACH BLUEBOOK'S OWN SOURCE, not one file read once and reused
35
+ # for every bluebook in the registry — true as long as a domain
36
+ # directory only ever held exactly one, and silently wrong the
37
+ # moment `uses_framework` made a second, differently-sourced
38
+ # bluebook (Governance, Identity — `lib/hecks/framework/bluebook/`,
39
+ # not the domain's own directory) share a boot with the first.
40
+ # Caught the hard way: three domains booted together, one real
41
+ # source text (the domain's own), and every OTHER domain's era-1
42
+ # held THAT text under its own name — a shadow-parse of it later
43
+ # reconstructs a completely different shape, and every boot after
44
+ # the first refuses toward a scaffold that was never the real
45
+ # drift.
46
+ def check!(registry, directory)
47
+ registry.bluebooks.each_value do |bluebook|
48
+ check_bluebook!(registry, bluebook, source_text_for(bluebook, directory), directory: directory)
49
+ end
50
+ end
51
+
52
+ # The domain's own directory first, matched by name — a real app's
53
+ # directory may hold more than one file once `uses_framework`
54
+ # exists, so ".first" alone can no longer be trusted, the exact
55
+ # way it silently wasn't the day this was found: three domains
56
+ # booted together, one real source text read once (the domain's
57
+ # own, ".first"'d), and every OTHER domain's era-1 held THAT text
58
+ # under its own name — a later shadow-parse of it reconstructs a
59
+ # completely different shape, and every boot after the first
60
+ # refuses toward a scaffold that was never the real drift.
61
+ #
62
+ # A single-file directory whose one file names something ELSE
63
+ # falls back to it anyway (a fixture may legitimately name its
64
+ # file differently from the `Hecks.bluebook` it declares) — UNLESS
65
+ # this bluebook is a known framework member, in which case that
66
+ # one file is certainly some OTHER domain's, not this one's, and
67
+ # the framework registry is asked instead — the only other place a
68
+ # bluebook in this registry could have come from, per
69
+ # `uses_framework`.
70
+ def source_text_for(bluebook, directory)
71
+ domain_files = Dir[File.join(directory, "*.bluebook")]
72
+ own = domain_files.select do |path|
73
+ File.foreach(path, encoding: "UTF-8").any? { |line| line.match?(/\A\s*Hecks\.bluebook\s+#{Regexp.escape(bluebook.name.inspect)}/) }
74
+ end
75
+ own = domain_files if own.empty? && domain_files.size == 1 && !Framework.members.key?(bluebook.name)
76
+ own = [Framework.members[bluebook.name]].compact if own.empty?
77
+ return if own.empty?
78
+
79
+ own.map { |path| File.read(path, encoding: "UTF-8") }.join("\n")
80
+ end
81
+
82
+ def check_bluebook!(registry, bluebook, current_text, directory: nil)
83
+ check_compute_rules!(registry, bluebook)
84
+
85
+ first = bluebook.aggregates.first
86
+ return unless first
87
+
88
+ adapter_name = adapter_for(registry, bluebook.name, first)
89
+ return unless lineage_capable?(registry, adapter_name)
90
+
91
+ unless current_text
92
+ raise WiringError,
93
+ "cannot boot #{bluebook.name}: bound to a lineage-capable adapter, but no source file for " \
94
+ "it could be found (checked #{directory.inspect} and the framework registry)"
95
+ end
96
+
97
+ settings = registry.world(bluebook.name)&.for_binding(Ports::Persistence::VERB, adapter_name) || {}
98
+ registry.adapter_class(adapter_name).era_check!(
99
+ registry: registry, bluebook: bluebook, current_text: current_text, settings: settings,
100
+ directory: directory
101
+ )
102
+ end
103
+
104
+ # The per-rule capability gate — NOT an era fact, and so it
105
+ # survives on every adapter: a compute rule's SQL is its only
106
+ # implementation, so an aggregate carrying one cannot boot
107
+ # anywhere but Postgres, whatever any shape comparison would say.
108
+ def check_compute_rules!(registry, bluebook)
109
+ bluebook.aggregates.each do |aggregate|
110
+ lineage = Ports::Persistence::Lineage.for(registry, bluebook.name, aggregate)
111
+ next unless lineage&.computes?
112
+
113
+ adapter = adapter_for(registry, bluebook.name, aggregate)
114
+ next if lineage_capable?(registry, adapter)
115
+
116
+ raise WiringError, "compute rules require the Postgres adapter; #{aggregate.name} is bound to #{adapter}"
117
+ end
118
+ end
119
+
120
+ def adapter_for(registry, domain, aggregate)
121
+ Ports::Persistence::BindingPolicy.resolve(registry, domain, aggregate).adapter
122
+ end
123
+
124
+ # The capability idiom: an adapter CLASS that answers
125
+ # lineage_capable? with true carries eras and may act on drift
126
+ # (translate, fork, merge). Postgres alone does today; the seam is
127
+ # what lets a second one arrive without touching this file.
128
+ def lineage_capable?(registry, adapter_name)
129
+ adapter_class = registry.adapters[adapter_name] && registry.adapter_class(adapter_name)
130
+ adapter_class.respond_to?(:lineage_capable?) && adapter_class.lineage_capable?
131
+ rescue StandardError
132
+ false
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,128 @@
1
+ module Hecks
2
+ module Runtime
3
+ module EraGuard
4
+ # The pure half: computing an aggregate's structural signature and
5
+ # diffing two eras' shapes. No file I/O — everything here answers
6
+ # from the IR it is handed, which is why the Postgres mint path can
7
+ # call it too.
8
+ module ShapeDiff
9
+ def shape(aggregate)
10
+ aggregate.attributes.map { |attribute|
11
+ [attribute.name, attribute_signature(aggregate, attribute.type)]
12
+ }.sort_by(&:first)
13
+ end
14
+
15
+ # A plain type name for a primitive; `[type_name, member_signatures]`
16
+ # for a value object or entity, walked recursively — so two
17
+ # attributes with the same declared type name but a different
18
+ # internal shape are never mistaken for unchanged.
19
+ def attribute_signature(aggregate, type_name, seen = [])
20
+ container = nested_type(aggregate, type_name)
21
+ return type_name if container.nil? || seen.include?(type_name)
22
+
23
+ members = container.attributes.map { |member|
24
+ [member.name, attribute_signature(aggregate, member.type, seen + [type_name])]
25
+ }.sort_by(&:first)
26
+ [type_name, members]
27
+ end
28
+
29
+ def nested_type(aggregate, type_name)
30
+ aggregate.value_object(type_name) || aggregate.entities.find { |entity| entity.name == type_name }
31
+ end
32
+
33
+ # Attributes present now that the held shape never had — the
34
+ # addition side of drift, which `uncovered_attributes` above never
35
+ # looks at, since it only walks the HELD shape (vanish/retype).
36
+ # Most additions are free (ADR 0025, "Added attributes and
37
+ # absence"): a default:, a list_of (frozen []), or a value object
38
+ # whose fields all default fill an existing record automatically
39
+ # via `Instance.hydrate_with_defaults`. Only the fourth case — a
40
+ # non-optional attribute with no way to fill itself — can leave an
41
+ # existing record with the field genuinely absent, and that is
42
+ # what this reports: unfilled by a declared translation's own
43
+ # `backfill`, OR by a move/convert that lands an OLD field inside
44
+ # this brand-new attribute (`Lineage#fills?` — the destination-
45
+ # side question, never `explains?`'s source-side one, since a
46
+ # rename from Crate to Bin can introduce a top-level attribute
47
+ # name that never existed to have "vanished").
48
+ def unsafe_additions(aggregate, held_aggregate, lineage)
49
+ held_names = held_aggregate.attributes.map(&:name)
50
+
51
+ aggregate.attributes
52
+ .reject { |attribute| held_names.include?(attribute.name) }
53
+ .select { |attribute| possibly_absent?(aggregate, attribute) }
54
+ .reject { |attribute| lineage&.fills?(attribute.name.to_s) }
55
+ .map(&:name)
56
+ end
57
+
58
+ # THE FOUR-ROW TABLE, as a predicate over the fourth row only — the
59
+ # other three (default:, list_of, a fully-defaulted value object)
60
+ # all fill an existing record for free and never reach here.
61
+ def possibly_absent?(aggregate, attribute)
62
+ return false if attribute.optional?
63
+ return false unless attribute.default.nil?
64
+ return false if attribute.list?
65
+
66
+ value_object = aggregate.value_object(attribute.type)
67
+ return false if value_object && value_object.attributes.all? { |field| !field.default.nil? }
68
+
69
+ true
70
+ end
71
+
72
+ # Paths the translation needs to explain: attributes that vanished
73
+ # by name, attributes that kept their name but changed type (a
74
+ # `convert` is what lets that be declared at all), and — recursing
75
+ # into a same-named, same-typed value object — its OWN members
76
+ # vanishing or changing type one level down, reported as a dotted
77
+ # path ("price.currency"). A pure addition, at any depth, never
78
+ # needs covering; only vanish-or-retype does, matching the
79
+ # top-level rule at every depth.
80
+ def uncovered_attributes(aggregate, held_aggregate, lineage)
81
+ paths = held_aggregate.attributes.flat_map do |held_attribute|
82
+ current_attribute = aggregate.attribute(held_attribute.name)
83
+ next [held_attribute.name.to_s] unless current_attribute
84
+
85
+ diff_type(held_attribute.name.to_s, held_attribute.type, current_attribute.type, held_aggregate, aggregate, lineage)
86
+ end
87
+
88
+ return paths unless lineage
89
+
90
+ paths.reject { |path| lineage.explains?(path) }
91
+ end
92
+
93
+ # `held_type`/`current_type` are type NAMES, resolved against each
94
+ # side's OWN value_object AND entity declarations — neither is ever
95
+ # nested in the DSL, only in the type graph, so both are always
96
+ # looked up flat off their respective aggregate. A `list_of` entity
97
+ # is only reached here to DETECT a member vanish-or-retype; there is
98
+ # no per-element translation machinery yet (`move`/`convert`/`drop`
99
+ # only reach into a single nested hash, not each element of an
100
+ # array) — the only way to satisfy a refusal on an entity path
101
+ # today is a top-level `drop` of the whole list attribute, which
102
+ # `explains?` already recognizes as covering everything nested
103
+ # under it. Blunt, but loud beats silent.
104
+ def diff_type(path, held_type, current_type, held_aggregate, aggregate, lineage, seen = [])
105
+ if held_type != current_type
106
+ # A declared retype says the two TYPE names mean the same shape
107
+ # — accept the pair, but still recurse into the members so a
108
+ # member drift hiding beneath the rename is caught by name.
109
+ return [path] unless lineage&.retype?(held_type, current_type)
110
+ end
111
+ return [] if seen.include?(current_type)
112
+
113
+ held_container = nested_type(held_aggregate, held_type)
114
+ current_container = nested_type(aggregate, current_type)
115
+ return [] unless held_container && current_container
116
+
117
+ held_container.attributes.flat_map do |held_member|
118
+ current_member = current_container.attribute(held_member.name)
119
+ next ["#{path}.#{held_member.name}"] unless current_member
120
+
121
+ diff_type("#{path}.#{held_member.name}", held_member.type, current_member.type, held_aggregate, aggregate, lineage,
122
+ seen + [current_type])
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end