hecks 2.7.0 → 2.9.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 (463) hide show
  1. checksums.yaml +4 -4
  2. data/exe/hecks +11 -0
  3. data/lib/hecks/adapters/driven/claude_code.rb +4 -104
  4. data/lib/hecks/adapters/driven/d1.rb +44 -237
  5. data/lib/hecks/adapters/driven/folder.rb +17 -76
  6. data/lib/hecks/adapters/driven/google_authentication.rb +15 -49
  7. data/lib/hecks/adapters/driven/governance_authorization.rb +23 -70
  8. data/lib/hecks/adapters/driven/heki/journal.rb +8 -41
  9. data/lib/hecks/adapters/driven/heki/saga_store.rb +12 -54
  10. data/lib/hecks/adapters/driven/heki/snapshot.rb +4 -12
  11. data/lib/hecks/adapters/driven/heki.rb +13 -42
  12. data/lib/hecks/adapters/driven/identity_registry.rb +3 -14
  13. data/lib/hecks/adapters/driven/in_memory_ordering.rb +9 -26
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +2 -14
  15. data/lib/hecks/adapters/driven/lambda/client.rb +10 -51
  16. data/lib/hecks/adapters/driven/lambda.rb +10 -84
  17. data/lib/hecks/adapters/driven/local_storage.rb +5 -40
  18. data/lib/hecks/adapters/driven/memory.rb +18 -99
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +5 -21
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +6 -28
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +3 -12
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +4 -30
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +34 -108
  24. data/lib/hecks/adapters/driven/postgres.rb +145 -134
  25. data/lib/hecks/adapters/driven/postgres_era.adapter +4 -15
  26. data/lib/hecks/adapters/driven/prism.rb +7 -30
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +21 -121
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +8 -40
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +26 -82
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +40 -84
  31. data/lib/hecks/adapters/driven/sqlite.rb +128 -65
  32. data/lib/hecks/adapters/driven/tenant_provisioner.rb +15 -37
  33. data/lib/hecks/adapters/driven.rb +5 -36
  34. data/lib/hecks/adapters/driving/github_webhook.rb +8 -76
  35. data/lib/hecks/behaviors/dsl.rb +7 -25
  36. data/lib/hecks/behaviors/expectations.rb +32 -142
  37. data/lib/hecks/behaviors/ir.rb +7 -20
  38. data/lib/hecks/behaviors/rspec.rb +6 -17
  39. data/lib/hecks/behaviors/runner.rb +10 -34
  40. data/lib/hecks/behaviors.rb +4 -8
  41. data/lib/hecks/bluebook/aggregate.rb +9 -75
  42. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +9 -38
  43. data/lib/hecks/bluebook/assembly/build.rb +4 -15
  44. data/lib/hecks/bluebook/assembly/contract.rb +15 -136
  45. data/lib/hecks/bluebook/assembly/contracts.rb +54 -174
  46. data/lib/hecks/bluebook/assembly/marks.rb +31 -223
  47. data/lib/hecks/bluebook/assembly/specializer.rb +6 -52
  48. data/lib/hecks/bluebook/assembly.rb +6 -45
  49. data/lib/hecks/bluebook/attribute.rb +12 -61
  50. data/lib/hecks/bluebook/behaviour/aggregate.rb +10 -39
  51. data/lib/hecks/bluebook/behaviour/attribute.rb +3 -14
  52. data/lib/hecks/bluebook/behaviour/chapter.rb +17 -42
  53. data/lib/hecks/bluebook/behaviour/command.rb +14 -66
  54. data/lib/hecks/bluebook/behaviour/domain_port.rb +4 -20
  55. data/lib/hecks/bluebook/behaviour/entity.rb +7 -27
  56. data/lib/hecks/bluebook/behaviour/hexagon.rb +23 -41
  57. data/lib/hecks/bluebook/behaviour/lifecycle.rb +15 -40
  58. data/lib/hecks/bluebook/behaviour/policy.rb +23 -52
  59. data/lib/hecks/bluebook/behaviour/process_manager.rb +30 -57
  60. data/lib/hecks/bluebook/behaviour/read_model.rb +38 -26
  61. data/lib/hecks/bluebook/behaviour/traits.rb +27 -64
  62. data/lib/hecks/bluebook/behaviour/value_object.rb +5 -24
  63. data/lib/hecks/bluebook/capabilities.rb +17 -49
  64. data/lib/hecks/bluebook/chapter.rb +10 -48
  65. data/lib/hecks/bluebook/command.rb +23 -81
  66. data/lib/hecks/bluebook/domain_port.rb +14 -63
  67. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +50 -229
  68. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +50 -377
  69. data/lib/hecks/bluebook/dsl/attribute_collector.rb +33 -204
  70. data/lib/hecks/bluebook/dsl/binding_proxy.rb +17 -65
  71. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +86 -708
  72. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +56 -170
  73. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +1 -0
  74. data/lib/hecks/bluebook/dsl/command_builder.rb +74 -546
  75. data/lib/hecks/bluebook/dsl/const_shim.rb +8 -41
  76. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +12 -87
  77. data/lib/hecks/bluebook/dsl/entity_builder.rb +44 -330
  78. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +20 -269
  79. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +26 -189
  80. data/lib/hecks/bluebook/dsl/identity_declaration.rb +13 -88
  81. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +6 -20
  82. data/lib/hecks/bluebook/dsl/policy_builder.rb +16 -88
  83. data/lib/hecks/bluebook/dsl/port_builder.rb +2 -6
  84. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +22 -96
  85. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +31 -184
  86. data/lib/hecks/bluebook/dsl/query_builder.rb +11 -42
  87. data/lib/hecks/bluebook/dsl/read_model_builder.rb +75 -201
  88. data/lib/hecks/bluebook/dsl/rule_reference.rb +22 -163
  89. data/lib/hecks/bluebook/dsl/translation_builder.rb +28 -114
  90. data/lib/hecks/bluebook/dsl/value_object_builder.rb +10 -124
  91. data/lib/hecks/bluebook/dsl/word_gate.rb +16 -162
  92. data/lib/hecks/bluebook/dsl/world_builder.rb +19 -106
  93. data/lib/hecks/bluebook/entity.rb +13 -71
  94. data/lib/hecks/bluebook/expression/ast_json.rb +24 -190
  95. data/lib/hecks/bluebook/expression/ast_reader.rb +12 -54
  96. data/lib/hecks/bluebook/expression/canonical_form.rb +11 -88
  97. data/lib/hecks/bluebook/expression/evaluator.rb +47 -295
  98. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +30 -147
  99. data/lib/hecks/bluebook/expression/resolver.rb +37 -660
  100. data/lib/hecks/bluebook/hexagon.rb +5 -28
  101. data/lib/hecks/bluebook/lifecycle.rb +5 -16
  102. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -7
  103. data/lib/hecks/bluebook/meta_validator/judge.rb +111 -361
  104. data/lib/hecks/bluebook/meta_validator/plan.rb +33 -162
  105. data/lib/hecks/bluebook/meta_validator/port_judge.rb +2 -7
  106. data/lib/hecks/bluebook/meta_validator/readings.rb +56 -342
  107. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -176
  108. data/lib/hecks/bluebook/meta_validator/shapes.rb +54 -268
  109. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +47 -308
  110. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +7 -43
  111. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -18
  112. data/lib/hecks/bluebook/meta_validator.rb +79 -349
  113. data/lib/hecks/bluebook/model_check/client_profile.rb +10 -91
  114. data/lib/hecks/bluebook/model_check.rb +102 -600
  115. data/lib/hecks/bluebook/pattern_subset.rb +11 -69
  116. data/lib/hecks/bluebook/process_manager.rb +13 -83
  117. data/lib/hecks/bluebook/project_loader.rb +3 -39
  118. data/lib/hecks/bluebook/project_register.rb +12 -60
  119. data/lib/hecks/bluebook/query.rb +9 -48
  120. data/lib/hecks/bluebook/read_model.rb +57 -81
  121. data/lib/hecks/bluebook/reference.rb +7 -41
  122. data/lib/hecks/bluebook/smoke_test.rb +8 -98
  123. data/lib/hecks/bluebook/synthesizer.rb +13 -62
  124. data/lib/hecks/bluebook/translation.rb +26 -62
  125. data/lib/hecks/bluebook/value_object.rb +11 -36
  126. data/lib/hecks/bluebook.rb +5 -22
  127. data/lib/hecks/cache_dir.rb +84 -0
  128. data/lib/hecks/cli/document.rb +86 -0
  129. data/lib/hecks/cli/ir.rb +45 -0
  130. data/lib/hecks/cli/mcp.rb +31 -0
  131. data/lib/hecks/cli/mcp_door.rb +366 -0
  132. data/lib/hecks/cli/model_check.rb +203 -0
  133. data/lib/hecks/cli/project_cli.rb +90 -0
  134. data/lib/hecks/cli/project_diagrams.rb +36 -0
  135. data/lib/hecks/cli/run.rb +173 -0
  136. data/lib/hecks/cli/smoke_test.rb +45 -0
  137. data/lib/hecks/cli/stores.rb +53 -0
  138. data/lib/hecks/cli.rb +160 -0
  139. data/lib/hecks/construct.rb +8 -34
  140. data/lib/hecks/deploy/bluebook/deploy.bluebook +33 -142
  141. data/lib/hecks/deploy/bluebook/deploy.hecksagon +2 -9
  142. data/lib/hecks/embryonaut_bluebook/lock.rb +3 -35
  143. data/lib/hecks/embryonaut_bluebook/shape.rb +1 -17
  144. data/lib/hecks/embryonaut_bluebook/vendor.rb +8 -53
  145. data/lib/hecks/embryonaut_bluebook/vendor_cli.rb +4 -19
  146. data/lib/hecks/embryonaut_bluebook.rb +4 -71
  147. data/lib/hecks/facade/cli_door.rb +24 -106
  148. data/lib/hecks/facade/cli_runner.rb +28 -172
  149. data/lib/hecks/facade/command_request.rb +13 -33
  150. data/lib/hecks/facade/handle.rb +34 -121
  151. data/lib/hecks/facade/json_door.rb +31 -146
  152. data/lib/hecks/facade/surface/aggregate_door.rb +39 -104
  153. data/lib/hecks/facade/surface/chapter.rb +10 -54
  154. data/lib/hecks/facade/surface.rb +8 -23
  155. data/lib/hecks/facade.rb +10 -25
  156. data/lib/hecks/forms/app.rb +15 -70
  157. data/lib/hecks/forms/command_form_renderer.rb +10 -29
  158. data/lib/hecks/forms/field_renderer.rb +17 -156
  159. data/lib/hecks/forms/field_shape.rb +30 -222
  160. data/lib/hecks/forms/html.rb +12 -47
  161. data/lib/hecks/forms/index_renderer.rb +1 -4
  162. data/lib/hecks/forms/page.rb +3 -9
  163. data/lib/hecks/forms/params.rb +14 -60
  164. data/lib/hecks/forms/port_argument.rb +2 -19
  165. data/lib/hecks/forms/query_form_renderer.rb +4 -79
  166. data/lib/hecks/forms/record_renderer.rb +8 -75
  167. data/lib/hecks/forms/record_table.rb +7 -50
  168. data/lib/hecks/forms/reference_options.rb +3 -30
  169. data/lib/hecks/forms/value_object_shape.rb +8 -35
  170. data/lib/hecks/forms.rb +5 -33
  171. data/lib/hecks/fqn.rb +5 -12
  172. data/lib/hecks/framework/bluebook/compliance.bluebook +16 -65
  173. data/lib/hecks/framework/bluebook/console_settings.bluebook +47 -134
  174. data/lib/hecks/framework/bluebook/framework.hecksagon +5 -14
  175. data/lib/hecks/framework/bluebook/governance.bluebook +13 -34
  176. data/lib/hecks/framework/bluebook/identity.bluebook +11 -26
  177. data/lib/hecks/framework/bluebook/privacy.bluebook +8 -33
  178. data/lib/hecks/framework.rb +11 -80
  179. data/lib/hecks/freezer.rb +5 -39
  180. data/lib/hecks/grammar/expression.bluebook +15 -40
  181. data/lib/hecks/grammar/translation.bluebook +3 -6
  182. data/lib/hecks/grammar.rb +16 -41
  183. data/lib/hecks/ir.rb +14 -83
  184. data/lib/hecks/language/adapter.bluebook +2 -7
  185. data/lib/hecks/language/bluebook/aggregate.bluebook +56 -221
  186. data/lib/hecks/language/bluebook/attaches/paging.bluebook +6 -15
  187. data/lib/hecks/language/bluebook/bluebook.bluebook +15 -83
  188. data/lib/hecks/language/bluebook/bluebook.hecksagon +3 -15
  189. data/lib/hecks/language/bluebook/command.bluebook +27 -144
  190. data/lib/hecks/language/bluebook/entity.bluebook +24 -97
  191. data/lib/hecks/language/bluebook/policy.bluebook +16 -67
  192. data/lib/hecks/language/bluebook/process_manager.bluebook +28 -151
  193. data/lib/hecks/language/bluebook/projection.bluebook +126 -60
  194. data/lib/hecks/language/bluebook/query.bluebook +12 -43
  195. data/lib/hecks/language/bluebook/shape.bluebook +23 -73
  196. data/lib/hecks/language/bluebook/syntax.bluebook +51 -262
  197. data/lib/hecks/language/bluebook/vocabulary.bluebook +98 -197
  198. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +3 -10
  199. data/lib/hecks/language/hecksagon/domain_port.bluebook +4 -19
  200. data/lib/hecks/language/hecksagon/hecksagon.bluebook +6 -25
  201. data/lib/hecks/language/hecksagon/port_operation.bluebook +3 -14
  202. data/lib/hecks/language/oidc.json +35 -0
  203. data/lib/hecks/language/port.bluebook +4 -17
  204. data/lib/hecks/language/translation/translation.bluebook +3 -9
  205. data/lib/hecks/language/translation/translation_aggregate.bluebook +5 -24
  206. data/lib/hecks/language/world/wiring.bluebook +1 -3
  207. data/lib/hecks/language/world/world.bluebook +5 -7
  208. data/lib/hecks/literal.rb +8 -50
  209. data/lib/hecks/mcp_door_scope.rb +95 -0
  210. data/lib/hecks/mcp_stdio_guard.rb +4 -41
  211. data/lib/hecks/naming.rb +29 -137
  212. data/lib/hecks/ports/access_control.rb +7 -23
  213. data/lib/hecks/ports/agent/answers.rb +5 -23
  214. data/lib/hecks/ports/agent.rb +47 -172
  215. data/lib/hecks/ports/authentication.rb +12 -39
  216. data/lib/hecks/ports/authorization.rb +16 -43
  217. data/lib/hecks/ports/clock.rb +5 -45
  218. data/lib/hecks/ports/extraction.rb +7 -12
  219. data/lib/hecks/ports/identity_assignment.rb +8 -29
  220. data/lib/hecks/ports/identity_generation.rb +6 -29
  221. data/lib/hecks/ports/identity_resolution.rb +2 -10
  222. data/lib/hecks/ports/key_vault.rb +9 -45
  223. data/lib/hecks/ports/persistence/append_only.rb +62 -65
  224. data/lib/hecks/ports/persistence/binding_policy.rb +4 -14
  225. data/lib/hecks/ports/persistence/codec_boundary.rb +20 -80
  226. data/lib/hecks/ports/persistence/null_saga_store.rb +4 -20
  227. data/lib/hecks/ports/persistence/plugin.rb +13 -42
  228. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +38 -141
  229. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +15 -115
  230. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +16 -84
  231. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +7 -26
  232. data/lib/hecks/ports/persistence/plugins/era/expected_era.rb +3 -22
  233. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +55 -181
  234. data/lib/hecks/ports/persistence/plugins/era/postgres_era/events.rb +77 -0
  235. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +12 -122
  236. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +28 -128
  237. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +73 -474
  238. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +31 -124
  239. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +45 -219
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +13 -108
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +18 -75
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +9 -49
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +6 -123
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +23 -55
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +14 -72
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +7 -16
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +14 -115
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +13 -45
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +127 -554
  250. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +13 -63
  251. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +2 -17
  252. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +3 -14
  253. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +13 -58
  254. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +3 -17
  255. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +9 -26
  256. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +9 -25
  257. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +22 -122
  258. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +34 -123
  259. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +5 -12
  260. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +3 -9
  261. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -14
  262. data/lib/hecks/ports/persistence/plugins/era.rb +4 -23
  263. data/lib/hecks/ports/persistence/postgres_dump/connection.rb +7 -23
  264. data/lib/hecks/ports/persistence/postgres_dump.rb +4 -35
  265. data/lib/hecks/ports/persistence/remote_runtime.rb +2 -24
  266. data/lib/hecks/ports/persistence/repository_factory.rb +8 -27
  267. data/lib/hecks/ports/persistence/state_codec.rb +23 -103
  268. data/lib/hecks/ports/persistence.rb +7 -14
  269. data/lib/hecks/ports/projection.rb +42 -84
  270. data/lib/hecks/ports/query/in_memory.rb +10 -58
  271. data/lib/hecks/ports/query/ordering.rb +11 -32
  272. data/lib/hecks/ports/query.rb +14 -37
  273. data/lib/hecks/ports.rb +2 -8
  274. data/lib/hecks/projections/bootstrap_table.rb +11 -43
  275. data/lib/hecks/projections/deploy/fargate/assembly.rb +2 -15
  276. data/lib/hecks/projections/deploy/fargate/cdn.rb +1 -48
  277. data/lib/hecks/projections/deploy/fargate/cdn_default.rb +2 -11
  278. data/lib/hecks/projections/deploy/fargate/check.rb +5 -14
  279. data/lib/hecks/projections/deploy/fargate/containers.rb +3 -78
  280. data/lib/hecks/projections/deploy/fargate/extras.rb +3 -44
  281. data/lib/hecks/projections/deploy/fargate/monitoring.rb +0 -44
  282. data/lib/hecks/projections/deploy/fargate/settings.rb +2 -54
  283. data/lib/hecks/projections/deploy/fargate/yaml.rb +13 -58
  284. data/lib/hecks/projections/deploy/fargate.rb +35 -202
  285. data/lib/hecks/projections/deploy/lambda.rb +151 -679
  286. data/lib/hecks/projections/deploy/preview/access_yaml.rb +1 -33
  287. data/lib/hecks/projections/deploy/preview/containers.rb +6 -111
  288. data/lib/hecks/projections/deploy/preview/routing_yaml.rb +4 -56
  289. data/lib/hecks/projections/deploy/preview/script.rb +2 -14
  290. data/lib/hecks/projections/deploy/preview/settings.rb +3 -33
  291. data/lib/hecks/projections/deploy/preview/task_yaml.rb +2 -38
  292. data/lib/hecks/projections/deploy/preview/template.rb +2 -58
  293. data/lib/hecks/projections/deploy/preview.rb +2 -67
  294. data/lib/hecks/projections/deploy/scripts/settings.rb +3 -25
  295. data/lib/hecks/projections/deploy/scripts.rb +4 -57
  296. data/lib/hecks/projections/deploy/shared.rb +47 -146
  297. data/lib/hecks/projections/deploy/smoke.rb +4 -61
  298. data/lib/hecks/projections/deploy/template_diff/comparer.rb +3 -25
  299. data/lib/hecks/projections/deploy/template_diff/loader.rb +2 -11
  300. data/lib/hecks/projections/deploy/template_diff.rb +5 -57
  301. data/lib/hecks/projections/diagrams.rb +20 -582
  302. data/lib/hecks/projections/glossary/html.rb +14 -130
  303. data/lib/hecks/projections/glossary/markdown.rb +8 -69
  304. data/lib/hecks/projections/glossary/mermaid.rb +9 -66
  305. data/lib/hecks/projections/glossary/sensitivity.rb +5 -34
  306. data/lib/hecks/projections/glossary/sentences.rb +14 -159
  307. data/lib/hecks/projections/glossary.rb +30 -222
  308. data/lib/hecks/projections/ir.rb +3 -10
  309. data/lib/hecks/projections/model/deviations.rb +15 -42
  310. data/lib/hecks/projections/model.rb +12 -51
  311. data/lib/hecks/projections/oidc.rb +12 -56
  312. data/lib/hecks/projections/parser_table.rb +9 -28
  313. data/lib/hecks/projections/reference.rb +9 -16
  314. data/lib/hecks/projections/rust_vocabulary.rb +12 -250
  315. data/lib/hecks/projections/shape.rb +3 -29
  316. data/lib/hecks/projections/statements.rb +7 -86
  317. data/lib/hecks/projections/vocabulary.rb +6 -22
  318. data/lib/hecks/projections.rb +2 -16
  319. data/lib/hecks/projector/cli_projector.rb +60 -375
  320. data/lib/hecks/projector/docs_projector.rb +10 -201
  321. data/lib/hecks/projector/exporter.rb +46 -138
  322. data/lib/hecks/projector/ir_projector.rb +2 -9
  323. data/lib/hecks/projector/narrate_projector.rb +15 -189
  324. data/lib/hecks/projector/target.rb +13 -76
  325. data/lib/hecks/projector.rb +30 -121
  326. data/lib/hecks/query_specification/common/comparators.rb +5 -26
  327. data/lib/hecks/query_specification/common/comparison.rb +35 -143
  328. data/lib/hecks/query_specification/common/dsl.rb +8 -24
  329. data/lib/hecks/query_specification/common/null_policy.rb +20 -70
  330. data/lib/hecks/query_specification/common/options.rb +2 -6
  331. data/lib/hecks/query_specification/common/where_clause.rb +2 -10
  332. data/lib/hecks/query_specification/field_path.rb +37 -77
  333. data/lib/hecks/query_specification/hop_path.rb +12 -76
  334. data/lib/hecks/release/gem_pin.rb +2 -26
  335. data/lib/hecks/release/runner/ci_publisher.rb +93 -0
  336. data/lib/hecks/release/runner/commands.rb +66 -0
  337. data/lib/hecks/release/runner/console.rb +52 -0
  338. data/lib/hecks/release/runner/gem_publisher.rb +43 -0
  339. data/lib/hecks/release/runner/git.rb +48 -0
  340. data/lib/hecks/release/runner/npm_publisher.rb +75 -0
  341. data/lib/hecks/release/runner/preflight.rb +104 -0
  342. data/lib/hecks/release/runner/published.rb +55 -0
  343. data/lib/hecks/release/runner/tagger.rb +80 -0
  344. data/lib/hecks/release/runner.rb +215 -0
  345. data/lib/hecks/rendering.rb +4 -26
  346. data/lib/hecks/router/namespace_installer.rb +12 -36
  347. data/lib/hecks/router.rb +4 -8
  348. data/lib/hecks/runtime/aggregate_lock.rb +8 -33
  349. data/lib/hecks/runtime/boot_gates.rb +3 -18
  350. data/lib/hecks/runtime/caller.rb +19 -59
  351. data/lib/hecks/runtime/capability_graph.rb +5 -21
  352. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +16 -78
  353. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +23 -160
  354. data/lib/hecks/runtime/command_interpreter.rb +125 -307
  355. data/lib/hecks/runtime/command_rules/admissibility.rb +95 -366
  356. data/lib/hecks/runtime/command_rules/arithmetic.rb +66 -196
  357. data/lib/hecks/runtime/command_rules/authorization.rb +13 -45
  358. data/lib/hecks/runtime/command_rules/emission.rb +8 -21
  359. data/lib/hecks/runtime/command_rules/references.rb +23 -202
  360. data/lib/hecks/runtime/dependency_planning.rb +30 -102
  361. data/lib/hecks/runtime/dispatcher.rb +82 -241
  362. data/lib/hecks/runtime/entity_element.rb +40 -462
  363. data/lib/hecks/runtime/entity_interpreter.rb +65 -220
  364. data/lib/hecks/runtime/errors.rb +32 -115
  365. data/lib/hecks/runtime/event.rb +3 -25
  366. data/lib/hecks/runtime/identity.rb +18 -154
  367. data/lib/hecks/runtime/instance.rb +18 -150
  368. data/lib/hecks/runtime/interpreting.rb +11 -70
  369. data/lib/hecks/runtime/invocation.rb +28 -164
  370. data/lib/hecks/runtime/loader.rb +48 -186
  371. data/lib/hecks/runtime/outbox.rb +38 -261
  372. data/lib/hecks/runtime/policy_interpreter.rb +38 -200
  373. data/lib/hecks/runtime/port_operation_interpreter.rb +26 -87
  374. data/lib/hecks/runtime/query_interpreter.rb +45 -187
  375. data/lib/hecks/runtime/reaction_invocation.rb +36 -138
  376. data/lib/hecks/runtime/read_model_interpreter.rb +139 -208
  377. data/lib/hecks/runtime/rebuild_sweep.rb +8 -54
  378. data/lib/hecks/runtime/reference_hop.rb +8 -81
  379. data/lib/hecks/runtime/refusal_wording.rb +8 -89
  380. data/lib/hecks/runtime/registry/saga_persistence.rb +10 -102
  381. data/lib/hecks/runtime/registry/verification.rb +52 -225
  382. data/lib/hecks/runtime/registry/world_defaults.rb +4 -53
  383. data/lib/hecks/runtime/registry.rb +103 -244
  384. data/lib/hecks/runtime/remote_dispatcher.rb +37 -152
  385. data/lib/hecks/runtime/routing.rb +4 -15
  386. data/lib/hecks/runtime/saga_interpreter/correlation.rb +14 -61
  387. data/lib/hecks/runtime/saga_interpreter.rb +79 -263
  388. data/lib/hecks/runtime/saga_pending_dispatch.rb +2 -43
  389. data/lib/hecks/runtime/tenant_check.rb +6 -68
  390. data/lib/hecks/runtime/tenant_scope.rb +3 -28
  391. data/lib/hecks/runtime/value/admission.rb +16 -67
  392. data/lib/hecks/runtime/value/coercion.rb +155 -641
  393. data/lib/hecks/runtime/value/entity_list_coercion.rb +26 -219
  394. data/lib/hecks/runtime/value.rb +12 -84
  395. data/lib/hecks/runtime.rb +17 -65
  396. data/lib/hecks/storehouse.rb +146 -567
  397. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +8 -32
  398. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +6 -23
  399. data/lib/hecks/vendoring/git_environment.rb +2 -8
  400. data/lib/hecks/vendoring/git_source.rb +4 -10
  401. data/lib/hecks/vendoring.rb +7 -46
  402. data/lib/hecks/version.rb +3 -14
  403. data/lib/hecks/vocabulary.rb +5 -0
  404. data/lib/hecks.rb +32 -82
  405. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -46
  406. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +11 -80
  407. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +10 -99
  408. metadata +30 -59
  409. data/lib/hecks/bench/cli.rb +0 -74
  410. data/lib/hecks/bench/environment.rb +0 -100
  411. data/lib/hecks/bench/postgres_probe.rb +0 -41
  412. data/lib/hecks/bench/report.rb +0 -116
  413. data/lib/hecks/bench/ruby_runner.rb +0 -151
  414. data/lib/hecks/bench/run.rb +0 -55
  415. data/lib/hecks/bench/rust_runner.rb +0 -165
  416. data/lib/hecks/bench/stats.rb +0 -75
  417. data/lib/hecks/bench/suite.rb +0 -172
  418. data/lib/hecks/bench/workload.rb +0 -137
  419. data/lib/hecks/bench.rb +0 -28
  420. data/lib/hecks/codemod.rb +0 -443
  421. data/lib/hecks/corpus.rb +0 -479
  422. data/lib/hecks/doc/reference.rb +0 -585
  423. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +0 -684
  424. data/lib/hecks/fuzzing/combination_miner.rb +0 -178
  425. data/lib/hecks/fuzzing/concurrent_dispatch.rb +0 -469
  426. data/lib/hecks/fuzzing/coverage_campaign.rb +0 -161
  427. data/lib/hecks/fuzzing/differential.rb +0 -192
  428. data/lib/hecks/fuzzing/domain_generator.rb +0 -871
  429. data/lib/hecks/fuzzing/era_boundary.rb +0 -154
  430. data/lib/hecks/fuzzing/form_census.rb +0 -285
  431. data/lib/hecks/fuzzing/generated_domain_check.rb +0 -171
  432. data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -125
  433. data/lib/hecks/fuzzing/isolated_boot.rb +0 -658
  434. data/lib/hecks/fuzzing/nondeterministic.rb +0 -79
  435. data/lib/hecks/fuzzing/persistence_parity.rb +0 -253
  436. data/lib/hecks/fuzzing/properties/corrections.rb +0 -125
  437. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +0 -688
  438. data/lib/hecks/fuzzing/properties/guards.rb +0 -398
  439. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +0 -399
  440. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +0 -137
  441. data/lib/hecks/fuzzing/properties/outbox.rb +0 -180
  442. data/lib/hecks/fuzzing/properties/querying.rb +0 -260
  443. data/lib/hecks/fuzzing/properties.rb +0 -267
  444. data/lib/hecks/fuzzing/qa_settings.rb +0 -164
  445. data/lib/hecks/fuzzing/replay.rb +0 -930
  446. data/lib/hecks/fuzzing/rotation_priority.rb +0 -114
  447. data/lib/hecks/fuzzing/rust_gap_manifest.rb +0 -139
  448. data/lib/hecks/fuzzing/self_consistency.rb +0 -825
  449. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +0 -532
  450. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +0 -171
  451. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +0 -127
  452. data/lib/hecks/fuzzing/sequence_generator/picker.rb +0 -116
  453. data/lib/hecks/fuzzing/sequence_generator/query_binding.rb +0 -138
  454. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +0 -344
  455. data/lib/hecks/fuzzing/sequence_generator.rb +0 -311
  456. data/lib/hecks/fuzzing/shrinker.rb +0 -309
  457. data/lib/hecks/fuzzing/structural_skips.rb +0 -53
  458. data/lib/hecks/fuzzing/sweep_depth.rb +0 -61
  459. data/lib/hecks/fuzzing/target_capabilities.rb +0 -255
  460. data/lib/hecks/fuzzing/value_generator.rb +0 -340
  461. data/lib/hecks/fuzzing.rb +0 -23
  462. data/lib/hecks/grammar/evolve.rb +0 -524
  463. data/lib/hecks/query_ir.rb +0 -482
@@ -6,28 +6,10 @@ require_relative "../../runtime/value"
6
6
 
7
7
  module Hecks
8
8
  module Adapters
9
- # The one SQL query compilation, shared by the SQLite and Postgres
10
- # adapters — one walk rather than a near-line-identical copy per
11
- # adapter, because copies can only drift. Every declared operator
12
- # compiles fully into SQL or the query refuses loudly; the null-policy
13
- # predicate, the comma-separated `in` convention, and the identity
14
- # ORDER BY fallback are spelled once, here.
15
- #
16
- # What a dialect actually differs on is narrow, and each adapter
17
- # supplies exactly those hooks:
18
- #
19
- # placeholder(binds, value) — "?" or "$N", recording the bind
20
- # contains_clause(expr, ph) — instr() or position(), for a scalar field
21
- # list_contains_clause(name, member, ph) — real element containment,
22
- # for a `list_of` field (json_each /
23
- # jsonb_array_elements)
24
- # empty_in_clause — "0" or "FALSE"
25
- # comparable_expression(e, v) — ::numeric cast, where the dialect casts
26
- # plain_column(name) — a real column or a jsonb path
27
- # nested_expression(name, path, member) — json_extract or state #>>
28
- # order_clause(order_by, policy)
29
- # execute_query(sql, binds) — run it, return instances
30
- # dialect_name — for the refusal message
9
+ # The SQL query compilation shared by the SQLite and Postgres adapters. Each dialect
10
+ # supplies hooks: placeholder, contains_clause, list_contains_clause, empty_in_clause,
11
+ # comparable_expression, plain_column, nested_expression, order_clause, execute_query
12
+ # and dialect_name.
31
13
  module SqlQueryBuilder
32
14
  COMPARATORS = {
33
15
  "eq" => "=", "ne" => "<>", "gt" => ">", "gte" => ">=", "lt" => "<", "lte" => "<="
@@ -52,11 +34,7 @@ module Hecks
52
34
  sql << " WHERE #{clauses.join(' AND ')}" unless clauses.empty?
53
35
  sql << order_by_sql(declared)
54
36
  sql << " LIMIT #{placeholder(binds, query_value(declared.limit.value, args).to_i)}" if declared.limit
55
- # An offset with no declared limit — SQLite refuses a bare OFFSET
56
- # outright (LIMIT -1 is its unbounded idiom) while Postgres accepts
57
- # one, so the dialect supplies its own unbounded spelling. Found by
58
- # the adapter-agreement gate on its first run, not by either
59
- # adapter's own spec: each was self-consistent, they just disagreed.
37
+ # SQLite refuses a bare OFFSET, so the dialect spells its own unbounded limit.
60
38
  sql << unbounded_limit if !declared.limit && declared.offset
61
39
  sql << " OFFSET #{placeholder(binds, query_value(declared.offset.value, args).to_i)}" if declared.offset
62
40
  execute_query(sql, binds)
@@ -64,12 +42,7 @@ module Hecks
64
42
 
65
43
  private
66
44
 
67
- # Every declared `where` clause, compiled in declaration order — a
68
- # null-policy predicate short-circuits the ordinary comparator path
69
- # per clause, same as it did inline. `binds` is populated in place
70
- # (the same array `query` goes on to push LIMIT/OFFSET placeholders
71
- # into afterward), so pulling this loop out changes nothing about
72
- # bind-parameter order.
45
+ # `binds` is filled in place so LIMIT/OFFSET placeholders follow the where binds.
73
46
  def where_clauses(declared, args, binds)
74
47
  declared.wheres.each_with_object([]) do |clause, clauses|
75
48
  value = query_value(clause.value, args)
@@ -103,71 +76,26 @@ module Hecks
103
76
  members = in_members(value)
104
77
  return empty_in_clause if members.empty?
105
78
 
106
- # **Both sides as text**. `in` is a textual reading everywhere else
107
- # — Ports::Query::InMemory#holds? compares `held.to_s` against
108
- # stringified members, and `in_members` above stringifies its
109
- # own — so a numeric field was the one shape where the engines
110
- # could not agree: `in_members` binds '100', while the column
111
- # expression yields the number 100, and `100 IN ('100','300')`
112
- # is false in SQLite (a json_extract result carries no column
113
- # affinity to coerce it) where Memory matched. Casting the
114
- # expression rather than typing the members keeps one rule for
115
- # every field: no type inference, and `in` means the same thing
116
- # on a String, an Integer, and a value-object member.
117
- #
118
- # `eq`/`gt`/`lt` never had this problem because they bind the
119
- # value's own type and route through `comparable_expression`,
120
- # which Postgres overrides to cast numerics.
79
+ # `in` reads as text everywhere: casting the column keeps a numeric field
80
+ # matching the stringified members (SQLite's json_extract carries no affinity).
121
81
  "CAST(#{expression} AS TEXT) IN (#{members.map { |member| placeholder(binds, member) }.join(', ')})"
122
82
  else
123
83
  raise ArgumentError, "#{dialect_name} query adapter does not support #{oper.inspect}"
124
84
  end
125
85
  end
126
86
 
127
- # A real array already says where its members end. Splitting one on
128
- # commas re-reads a boundary it already drew — and an id is a
129
- # domain value (Naming::IDENTITY_JOIN joins a composite identity's
130
- # own parts, and the parts it joins are whatever an identity path
131
- # pulled out of real data), so a name carrying a comma would
132
- # silently become two members matching the wrong rows, or nothing.
133
- # Calling `value.to_s` unconditionally would mean only a
134
- # comma-joined string ever works here, while
135
- # Ports::Query::InMemory and QueryInterpreter's own `members`
136
- # handle a real Array — three implementations of `in`,
137
- # two readings of it. Narrower than those two on one point,
138
- # deliberately: they unwrap a value-object/Hash element to its
139
- # own scalar first (`comparable`) before stringifying, a rule
140
- # this doesn't reproduce — every real caller of `in` today, and
141
- # the cross-aggregate hop this exists for, passes plain scalar
142
- # ids, never a value-object element.
87
+ # A real array is not re-split on commas: an id is a domain value and may hold one.
88
+ # Unlike the other engines, a Hash or value-object element is not unwrapped.
143
89
  def in_members(value)
144
90
  return value.map(&:to_s).reject(&:empty?) if value.is_a?(Array)
145
91
 
146
92
  value.to_s.split(",").map(&:strip).reject(&:empty?)
147
93
  end
148
94
 
149
- # Every declared field compiles into an expression over the stored
150
- # shape — or the query never runs. The member-picking rules are the
151
- # same in both dialects: a value-object field compares through the
152
- # member the bound value (or the declared types) say is numeric,
153
- # falling back to the one-field convention `value`.
154
- #
155
- # A reference is an ID, stored as a bare scalar (never wrapped —
156
- # `Runtime::Value.refuse_object_reference` guarantees it), so it
157
- # takes the same plain-column path as any other non-value-object
158
- # attribute. Excluding it into the value-object member-picking logic
159
- # compiles a nested read against a row that has no nested key — a
160
- # path that can never match. Measured, not assumed: a `where` on a
161
- # has_one/belongs_to/reference_to field returned zero rows against
162
- # real data until that exclusion was removed.
163
- # `member` resolves through two ordered fallback tiers — the bound
164
- # value's own numeric field first, then the declared value object's
165
- # numeric-or-sole attribute — each documented above as fixing a
166
- # real, measured bug (a reference field matching nothing, a
167
- # single-attribute VO silently falling to the wrong convention).
168
- # Splitting the tiers apart would separate two writes to the same
169
- # `member` local across method boundaries, hiding the fallback
170
- # order that makes them correct together.
95
+ # Compiles a declared field into an expression over the stored shape. A value-object
96
+ # field compares through its numeric member, else its sole attribute, else `value`.
97
+ # A reference is a bare id, so it takes the plain-column path.
98
+ # The two member fallbacks stay in one method so their order is visible.
171
99
  # rubocop:disable-next Metrics/CyclomaticComplexity
172
100
  # rubocop:disable-next Metrics/PerceivedComplexity
173
101
  def query_expression(field, value: nil)
@@ -180,40 +108,20 @@ module Hecks
180
108
  member = if path.empty? && value
181
109
  hash = value.is_a?(Runtime::Value) ? value.to_h : value
182
110
  numeric = hash.is_a?(Hash) && hash.find { |_key, item| item.is_a?(Numeric) }
183
- # Not &.-able: `numeric` can be `false` (hash.is_a?(Hash) came
184
- # back false) as well as nil (.find came back empty) — `&.`
185
- # only guards nil, so `false.first` raises. False positive.
111
+ # `numeric` can be false, which `&.` does not guard.
186
112
  # rubocop:disable-next Style/SafeNavigation
187
113
  numeric ? numeric.first : nil
188
114
  end
189
115
  member ||= if path.empty? && attribute && value_object?(attribute)
190
116
  object = Runtime::Value.value_object_for(@aggregate, attribute.type)
191
- # A sole attribute is the fallback when no member is
192
- # numeric — a single-attribute value object is its one
193
- # field whatever that field is named (`Behaviour::
194
- # ValueObject#sole_attribute`, the same strict rule
195
- # `Runtime::Value`'s own `.value` alias enforces), so
196
- # `EmailAddress{address}` compiles to `$.address`
197
- # rather than falling through to the dialects' own
198
- # `member || "value"` convention and silently matching
199
- # nothing. Multi-field non-numeric shapes still fall
200
- # through to that convention, unchanged — there is no
201
- # single field to honestly pick for them here either.
117
+ # A single-attribute value object is its one field, whatever it is named.
202
118
  (Forms::ValueObjectShape.numeric_member(object) || object.sole_attribute)&.name
203
119
  end
204
120
  nested_expression(name, path, member)
205
121
  end
206
122
 
207
- # A numeric member wins if the value object has one (Price, Money —
208
- # what every ordered comparison in the corpus until now compared),
209
- # otherwise the same single-field fallback `query_expression`'s own
210
- # `nested_expression(name, path, member || "value")` already
211
- # makes for the column side — a plain `attribute :value, String`
212
- # value object (IdentityId, RoleName, ...) has no numeric member at
213
- # all, and returning nil there silently turned an equality
214
- # comparison into `IS NULL`, matching nothing. Both sides of one
215
- # comparison have to agree on which field they mean, or they
216
- # compare two different things and call it a where clause.
123
+ # Picks the same member `query_expression` does, so both sides of a comparison
124
+ # agree on which field they mean.
217
125
  def query_value(value, args)
218
126
  value = args[value] if value.is_a?(Symbol)
219
127
  hash = value.is_a?(Runtime::Value) ? value.to_h : value
@@ -233,17 +141,9 @@ module Hecks
233
141
  !attr.list? && !Runtime::Value.value_object_for(@aggregate, attr.type).nil?
234
142
  end
235
143
 
236
- # `contains` on a `list_of` field means real element membership, not
237
- # a substring search over the column's raw JSON text — the reading
238
- # QueryInterpreter#holds? and Ports::Query::InMemory already give a
239
- # real Array, and a substring search would disagree with it (a
240
- # substring match over `[{"value":"not_high_risk"}]` falsely matches
241
- # "high_risk"). Returns nil for a non-list field (the caller falls
242
- # back to `contains_clause`, unchanged), "" for a list of bare
243
- # scalars (no member to walk into), or the one field name a
244
- # single-field value-object element carries. A list of a
245
- # multi-field value object has no one scalar to compare against and
246
- # is refused rather than guessed.
144
+ # `contains` on a list means element membership, not a substring search over JSON text.
145
+ # Returns nil for a non-list field, "" for bare scalars, else the sole member name.
146
+ # Raises for a multi-field value object.
247
147
  def list_member(field)
248
148
  return nil if field.to_s.include?(".")
249
149
 
@@ -16,18 +16,9 @@ module Hecks
16
16
  fields << { name: lifecycle.field, attribute: nil, sql_type: "TEXT" } if lifecycle && fields.none? do |field|
17
17
  field[:name] == lifecycle.field
18
18
  end
19
- # `projects` fields (S12, ADR 0025) are a local column too —
20
- # `CommandInterpreter#seed_projected_fields`/`RebuildSweep`
21
- # both write one straight into `Instance#state` the same as
22
- # any other field, so a column has to exist to hold it or
23
- # `save`'s own `columns =`/`values =` build (this file, not
24
- # here) silently drops it on every write. No `attribute` of
25
- # their own to carry (`ProjectedField` is a bare name/
26
- # reference/remote_field triple, not a typed attribute) —
27
- # treated as a raw scalar, same as the lifecycle field just
28
- # above: every real corpus use projects a status/lifecycle
29
- # string, and `encode_field`/`decode` both already pass a
30
- # `attribute: nil` field through untouched, not JSON-encoded.
19
+ # `projects` fields are written straight into `Instance#state`, so they need a column
20
+ # or `project` drops them. They carry no attribute and are stored as raw scalars,
21
+ # like the lifecycle field.
31
22
  @aggregate.projected_fields.each do |field|
32
23
  next if fields.any? { |f| f[:name] == field.name }
33
24
 
@@ -37,13 +28,8 @@ module Hecks
37
28
  end
38
29
 
39
30
  def encode(attr, value)
40
- # **Never set is not empty**. `then_set ... append:` starts a list at `[]`
41
- # the moment the first element lands, but a list attribute nothing has
42
- # ever appended to — the shape every other aggregate in the corpus
43
- # happened not to carry, until one declared a list-typed head
44
- # attribute that no creating command populates by default — has to
45
- # stay NULL to answer the same as Memory does. Forcing it to `[]`
46
- # here is how a persistence topology invented data no dispatch wrote.
31
+ # A list nothing has appended to stays NULL, as under Memory; forcing `[]` would
32
+ # invent data no dispatch wrote.
47
33
  return (value.nil? ? nil : state_json(value)) if attr.list?
48
34
  return state_json(value) if value.is_a?(Hash) || value.is_a?(Runtime::Value)
49
35
 
@@ -56,14 +42,8 @@ module Hecks
56
42
  encode(field[:attribute], value)
57
43
  end
58
44
 
59
- # Every JSON column's text goes through the state codec's `encode`
60
- # (PR A3) — string keys at every depth, Values materialized.
61
45
  def state_json(value) = JSON.generate(Ports::Persistence::StateCodec.encode(@aggregate, value))
62
46
 
63
- # The row's columns reassembled into the stored state, then decoded
64
- # once through the state codec (PR A3) — the same deep, IR-driven
65
- # key spelling every other adapter's read now produces, instead of
66
- # this codec's own `symbolize_names:` walk.
67
47
  def decode(row)
68
48
  state = persisted_fields.each_with_object({}) do |field, raw_state|
69
49
  attr = field[:attribute]
@@ -79,27 +59,15 @@ module Hecks
79
59
  if attr.list? || value_object?(attr)
80
60
  raw ? JSON.parse(raw) : nil
81
61
  else
82
- # A reference lands here now, with the ordinary scalars. It holds
83
- # the id of a head, which is text — `JSON.parse("acct-1")` raises,
84
- # so reading it as JSON was only ever survivable while the column
85
- # held an object.
62
+ # A reference holds a head's id, which is text and would fail JSON.parse.
86
63
  raw
87
64
  end
88
65
  end
89
66
  Ports::Persistence::StateCodec.decode(@aggregate, state)
90
67
  end
91
68
 
92
- # A NULL `projects` column is a field never seeded, not a stored
93
- # NIL — so it reads back absent, the way Heki/PostgresEra (one blob
94
- # holding only what was written) already answer it. A column holds
95
- # every persisted field whether or not the record ever had one, so
96
- # NULL is the only spelling "never set" has here; and nothing ever
97
- # stores a nil projected value to confuse it with —
98
- # `CommandInterpreter#seed_projected_fields` and
99
- # `RebuildSweep#refresh` both skip a nil remote value. Absent is also
100
- # what `RebuildSweep#refresh`'s `record.key?` and
101
- # `Registry#projection_current?`'s row comparison expect. The
102
- # lifecycle field (the other `attribute: nil` column) keeps its NULL.
69
+ # A NULL `projects` column means never seeded, so it reads back absent as under Heki and
70
+ # PostgresEra; nothing stores a nil projected value. The lifecycle field keeps its NULL.
103
71
  def projected_only?(field)
104
72
  @aggregate.lifecycle&.field&.to_sym != field[:name].to_sym &&
105
73
  @aggregate.projected_fields.any? { |projected| projected.name.to_sym == field[:name].to_sym }
@@ -13,9 +13,7 @@ require_relative "../sqlite"
13
13
  module Hecks
14
14
  module Adapters
15
15
  # Rebuilds a read store from the authoritative journal, entry by entry.
16
- # A value object is written as its JSON object and a reference as the
17
- # bare id it holds — the same shapes the command path writes, so there
18
- # is no second representation to accept here any more.
16
+ # Values are written as JSON, references as bare ids — the shapes the command path writes.
19
17
  class SqliteProjection < Sqlite
20
18
  # Replaces or deletes the read store's row for one journal entry, encoding the entry's
21
19
  # state directly rather than through a `Runtime::Instance`.
@@ -36,68 +34,31 @@ module Hecks
36
34
  entry
37
35
  end
38
36
 
39
- # Execute a declared read model against the projected aggregate-head
40
- # tables. The report shape is assembled from SQL-selected rows, rather
41
- # than scanning repositories and matching references in Ruby.
37
+ # Executes a declared read model against the projected read-store tables.
42
38
  #
43
- # M19 (docs/audits/2026-08-10-main-bug-audit.md,
44
- # docs/audits/2026-08-11-bug-triage.md) — this agrees with
45
- # `Runtime::ReadModelInterpreter#project` (the in-process path) on
46
- # the two counts where a native path can most easily diverge:
39
+ # A missing root raises, matching the in-process interpreter; a head
40
+ # matches against any already-projected source, not only the root.
47
41
  #
48
- # **Missing root**: the in-process path's own `fetch` refuses with
49
- # `NotFound` when the reference argument names no record, and so
50
- # does this. Answering a silent `{root: nil, ...}` instead would
51
- # make this the one path where a caller could dispatch a read model
52
- # against a record that never existed and get back something that
53
- # looks like an empty report rather than the refusal every other
54
- # path gives.
55
- #
56
- # **Chained-include join scope**: matching a non-root head against
57
- # the root's own id alone, regardless of what it actually
58
- # references, is correct for a head that references the root
59
- # directly and silently empty for one that references another
60
- # included head instead (`Leaf` -> `Mid` -> `Root`, `Leaf` itself
61
- # has no attribute referencing `Root` at all, so its `references`
62
- # would always be `[]`). The in-process path
63
- # (`ReadModelInterpreter#project`'s "root first, always" comment)
64
- # matches a head against any already-projected source, not
65
- # only the root — `select_related` does the same: each head is
66
- # matched against every source resolved so far (root first, then
67
- # declared order — the same one-level-of-declaration-order
68
- # dependency the in-process path itself has, documented
69
- # there as L2, not a gap introduced here).
70
- #
71
- # @param _domain [String, Symbol] name of the domain declaring the read model; not read
72
- # @param model [Bluebook::ReadModel, Runtime::TenantScope::Scoped] the read model to
73
- # answer, or the tenant-scoping delegator around one; must have a reference target
74
- # @param args [Hash{Symbol => Object}] the read model's arguments; the entry under
75
- # `model.reference_name` is the root record's id
76
- # @param bluebook [Bluebook::Chapter, nil] the domain's bluebook, which resolves each
77
- # included aggregate by name; nil is refused
78
- # @return [Array<Hash>] a one-element Array holding the report Hash, keyed by each
79
- # head's `as` name: an Array of plain state Hashes (each with `:id`) for a `many`
80
- # head, otherwise one such Hash, or nil when that head matched no row
81
- # @raise [ArgumentError] if `bluebook` is nil
82
- # @raise [KeyError] if `args` has no entry for the model's reference argument
83
- # @raise [Runtime::NotFound] if no projected row has the referenced root id
84
- # @raise [SQLite3::Exception] if a statement fails
42
+ # @param _domain [String, Symbol] domain name; not read
43
+ # @param model [Bluebook::ReadModel, Runtime::TenantScope::Scoped] read model to answer
44
+ # @param args [Hash] arguments; `model.reference_name` holds the root id
45
+ # @param bluebook [Bluebook::Chapter, nil] resolves each included aggregate; nil refused
46
+ # @return [Array<Hash>] one-element Array of the report, keyed by each head's `as`
47
+ # @raise [ArgumentError] if bluebook is nil
48
+ # @raise [Runtime::NotFound] if no projected row matches the referenced root id
85
49
  def query_read_model(_domain, model, args, bluebook = nil)
86
50
  raise ArgumentError, "projection query needs its domain bluebook" unless bluebook
87
51
 
88
- # The reference's own shape, read the same way ReadModelInterpreter
89
- # reads it — not the identity unwrap, which is gone : an identity is
90
- # declared as a path and followed.
52
+ # Read the same way ReadModelInterpreter reads it: an identity is a
53
+ # declared path, not unwrapped.
91
54
  reference_id = args.fetch(model.reference_name).to_s
92
55
  # Plural (ADR 0055) — `on:` lets `where`/`order_by`/`limit`/`offset`
93
- # each name a different many-side head, so more than one can be
94
- # eligible in the same read model now.
56
+ # each name a different many-side head, so more than one may be
57
+ # eligible in the same read model.
95
58
  eligible = model.filtered_head_names
96
59
 
97
- # **Root first, always** — see this method's own header. Mirrors
98
- # `ReadModelInterpreter#project`'s identical partition, for the
99
- # identical reason: a later head's own join has to be able to
100
- # match against a root (or another head) already resolved.
60
+ # Root first, always (see this method's own header): a later head's
61
+ # join must match against a root or head already resolved.
101
62
  root_heads, other_heads = model.aggregate_heads.partition { |head| head[:aggregate] == model.reference_target }
102
63
  projected = []
103
64
  reports = {}
@@ -130,12 +91,8 @@ module Hecks
130
91
  row && projected_instance(aggregate, row)
131
92
  end
132
93
 
133
- # Matched against every source already projected (root first, then
134
- # declared order — see this class's own `query_read_model` header),
135
- # not only the root — a head whose own reference points at another
136
- # included head rather than the root directly would otherwise match
137
- # nothing at all, since its reference attribute would be compared
138
- # against a target (the root) it never names.
94
+ # Matches every already-projected source, not just the root, so a head
95
+ # referencing another included head still matches.
139
96
  def select_related(aggregate, projected)
140
97
  matches = projected.flat_map do |source|
141
98
  references = aggregate.attributes.select do |attribute|
@@ -150,10 +107,7 @@ module Hecks
150
107
  end
151
108
  return [] if matches.empty?
152
109
 
153
- # A reference column holds the ID, so it compares as itself. The
154
- # `json_extract(col,'$.value') = ? OR col = ?` this replaced was
155
- # reading both shapes because both existed — one written by the
156
- # command path, one by older journals. There is one shape now.
110
+ # A reference column holds the id, so it compares directly against itself.
157
111
  clauses = matches.map { |attribute, _id| "#{quote_ident(attribute.name)} = ?" }
158
112
  bind = matches.map { |_attribute, id| id }
159
113
  @db.execute("SELECT * FROM #{quote_ident(aggregate.storage_name)} WHERE #{clauses.join(' OR ')} ORDER BY id", bind)
@@ -168,37 +122,27 @@ module Hecks
168
122
  decode_fields(fields_for(aggregate), aggregate, row)
169
123
  end
170
124
 
171
- # The field list a row decodes against: every declared attribute,
172
- # plus the lifecycle field and any `projects` fields not already
173
- # among them (both read back raw — see `decode_fields`).
125
+ # Declared attributes plus the lifecycle field and any unlisted
126
+ # `projects` fields, all read back raw (see `decode_fields`).
174
127
  def fields_for(aggregate)
175
128
  fields = aggregate.attributes.map { |attribute| [attribute.name, attribute] }
176
129
  if (lifecycle = aggregate.lifecycle) && fields.none? { |name, _| name == lifecycle.field }
177
130
  fields << [lifecycle.field, nil]
178
131
  end
179
- # `projects` fields (S12, ADR 0025) need reading back too — `project`
180
- # (above) already writes one into its own column via `persisted_fields`
181
- # (`Codec#persisted_fields`, this class's own superclass module), but
182
- # this method built its own independent field list that never
183
- # consulted it — a column `project` populated correctly, silently
184
- # dropped on every read back out. Same raw-passthrough treatment as
185
- # the lifecycle field just above: `attribute: nil` down in the loop.
132
+ # `projects` fields (ADR 0025) read back raw too — see `persisted_fields`.
186
133
  aggregate.projected_fields.each do |field|
187
134
  fields << [field.name, nil] unless fields.any? { |name, _| name == field.name }
188
135
  end
189
136
  fields
190
137
  end
191
138
 
192
- # Decoded through the state codec (PR A3) against the row's own
193
- # aggregate — see Codec#decode, including why a NULL projected-only
194
- # column reads back absent.
139
+ # Decoded through the state codec against the row's own aggregate —
140
+ # see Codec#decode for why a NULL projected-only column reads back absent.
195
141
  def decode_fields(fields, aggregate, row)
196
142
  state = fields.each_with_object({}) do |(name, attribute), raw_state|
197
143
  raw = row[name.to_s]
198
144
  # rubocop:disable Lint/DuplicateBranch -- the nil-attribute and
199
- # reference-id branches both just answer `raw`, coincidentally, for
200
- # two unrelated reasons (see each branch's own comment); merging
201
- # them would blur that distinction.
145
+ # reference-id branches both just answer `raw`, for unrelated reasons.
202
146
  next if attribute.nil? && raw.nil? && aggregate.lifecycle&.field&.to_sym != name.to_sym
203
147
 
204
148
  raw_state[name] =