hecks 1.0.2 → 1.2.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 (247) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +3 -3
  3. data/lib/hecks/adapters/driven/d1.rb +48 -28
  4. data/lib/hecks/adapters/driven/folder.rb +10 -3
  5. data/lib/hecks/adapters/driven/google_authentication.rb +2 -4
  6. data/lib/hecks/adapters/driven/heki/journal.rb +1 -1
  7. data/lib/hecks/adapters/driven/heki.rb +3 -5
  8. data/lib/hecks/adapters/driven/in_memory_ordering.rb +4 -1
  9. data/lib/hecks/adapters/driven/lambda.rb +2 -2
  10. data/lib/hecks/adapters/driven/local_storage.adapter +3 -0
  11. data/lib/hecks/adapters/driven/local_storage.rb +120 -0
  12. data/lib/hecks/adapters/driven/memory.rb +48 -3
  13. data/lib/hecks/adapters/driven/postgres/codec.rb +3 -1
  14. data/lib/hecks/adapters/driven/postgres/outbox.rb +100 -0
  15. data/lib/hecks/adapters/driven/postgres/reconnect.rb +57 -0
  16. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +22 -2
  17. data/lib/hecks/adapters/driven/postgres.rb +45 -31
  18. data/lib/hecks/adapters/driven/prism.rb +11 -0
  19. data/lib/hecks/adapters/driven/sql_query_builder.rb +45 -20
  20. data/lib/hecks/adapters/driven/sqlite/codec.rb +3 -1
  21. data/lib/hecks/adapters/driven/sqlite/projection.rb +22 -3
  22. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +23 -1
  23. data/lib/hecks/adapters/driven/sqlite.rb +87 -8
  24. data/lib/hecks/adapters/driven.rb +4 -0
  25. data/lib/hecks/adapters.rb +4 -0
  26. data/lib/hecks/behaviors/dsl.rb +7 -0
  27. data/lib/hecks/behaviors/expectations.rb +32 -4
  28. data/lib/hecks/behaviors/rspec.rb +2 -0
  29. data/lib/hecks/behaviors/runner.rb +4 -0
  30. data/lib/hecks/bluebook/aggregate.rb +6 -5
  31. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +4 -4
  32. data/lib/hecks/bluebook/assembly/build.rb +6 -3
  33. data/lib/hecks/bluebook/assembly/contract.rb +5 -1
  34. data/lib/hecks/bluebook/assembly/contracts.rb +4 -0
  35. data/lib/hecks/bluebook/assembly/marks.rb +15 -4
  36. data/lib/hecks/bluebook/assembly.rb +1 -3
  37. data/lib/hecks/bluebook/attribute.rb +6 -0
  38. data/lib/hecks/bluebook/behaviour/policy.rb +18 -0
  39. data/lib/hecks/bluebook/behaviour/process_manager.rb +17 -1
  40. data/lib/hecks/bluebook/behaviour/read_model.rb +53 -8
  41. data/lib/hecks/bluebook/behaviour/traits.rb +1 -1
  42. data/lib/hecks/bluebook/command.rb +6 -5
  43. data/lib/hecks/bluebook/dsl/adapter_builder.rb +4 -1
  44. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +524 -0
  45. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +29 -477
  46. data/lib/hecks/bluebook/dsl/attribute_collector.rb +14 -6
  47. data/lib/hecks/bluebook/dsl/binding_proxy.rb +5 -0
  48. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +848 -0
  49. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +25 -784
  50. data/lib/hecks/bluebook/dsl/command_builder.rb +53 -13
  51. data/lib/hecks/bluebook/dsl/const_shim.rb +6 -0
  52. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +20 -7
  53. data/lib/hecks/bluebook/dsl/entity_builder.rb +27 -6
  54. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +12 -2
  55. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +8 -1
  56. data/lib/hecks/bluebook/dsl/identity_declaration.rb +24 -12
  57. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +40 -1
  58. data/lib/hecks/bluebook/dsl/policy_builder.rb +10 -1
  59. data/lib/hecks/bluebook/dsl/port_builder.rb +6 -1
  60. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +33 -12
  61. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +56 -12
  62. data/lib/hecks/bluebook/dsl/query_builder.rb +7 -1
  63. data/lib/hecks/bluebook/dsl/read_model_builder.rb +142 -19
  64. data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -1
  65. data/lib/hecks/bluebook/dsl/translation_builder.rb +13 -2
  66. data/lib/hecks/bluebook/dsl/value_object_builder.rb +8 -1
  67. data/lib/hecks/bluebook/dsl/word_gate.rb +5 -0
  68. data/lib/hecks/bluebook/dsl/world_builder.rb +13 -3
  69. data/lib/hecks/bluebook/entity.rb +5 -3
  70. data/lib/hecks/bluebook/expression/ast_json.rb +88 -9
  71. data/lib/hecks/bluebook/expression/ast_reader.rb +107 -0
  72. data/lib/hecks/bluebook/expression/canonical_form.rb +7 -0
  73. data/lib/hecks/bluebook/expression/evaluator.rb +64 -11
  74. data/lib/hecks/bluebook/expression/projection.json +12 -0
  75. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +4 -3
  76. data/lib/hecks/bluebook/expression/resolver.rb +83 -11
  77. data/lib/hecks/bluebook/expression.rb +1 -0
  78. data/lib/hecks/bluebook/hexagon.rb +10 -0
  79. data/lib/hecks/bluebook/lifecycle.rb +9 -0
  80. data/lib/hecks/bluebook/meta_validator/judge.rb +10 -4
  81. data/lib/hecks/bluebook/meta_validator/plan.rb +4 -0
  82. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +1 -1
  83. data/lib/hecks/bluebook/meta_validator/shapes.rb +3 -3
  84. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +7 -4
  85. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +42 -3
  86. data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -1
  87. data/lib/hecks/bluebook/meta_validator.rb +5 -5
  88. data/lib/hecks/bluebook/model_check.rb +119 -82
  89. data/lib/hecks/bluebook/pattern_subset.rb +9 -0
  90. data/lib/hecks/bluebook/policy.rb +2 -1
  91. data/lib/hecks/bluebook/process_manager.rb +6 -0
  92. data/lib/hecks/bluebook/project_register.rb +45 -25
  93. data/lib/hecks/bluebook/query.rb +4 -0
  94. data/lib/hecks/bluebook/read_model.rb +2 -2
  95. data/lib/hecks/bluebook/smoke_test.rb +5 -0
  96. data/lib/hecks/bluebook/synthesizer.rb +2 -1
  97. data/lib/hecks/bluebook/translation.rb +3 -0
  98. data/lib/hecks/bluebook/value_object.rb +2 -2
  99. data/lib/hecks/codemod.rb +13 -0
  100. data/lib/hecks/doc/reference.rb +17 -11
  101. data/lib/hecks/facade/cli_runner.rb +2 -2
  102. data/lib/hecks/facade/command_request.rb +11 -11
  103. data/lib/hecks/facade/handle.rb +3 -3
  104. data/lib/hecks/facade/surface/aggregate_door.rb +52 -38
  105. data/lib/hecks/facade.rb +4 -0
  106. data/lib/hecks/forms/app.rb +10 -6
  107. data/lib/hecks/forms/command_form_renderer.rb +3 -3
  108. data/lib/hecks/forms/field_renderer.rb +14 -9
  109. data/lib/hecks/forms/field_shape.rb +7 -2
  110. data/lib/hecks/forms/html.rb +5 -2
  111. data/lib/hecks/forms/index_renderer.rb +1 -1
  112. data/lib/hecks/forms/page.rb +7 -3
  113. data/lib/hecks/forms/query_form_renderer.rb +3 -2
  114. data/lib/hecks/forms/record_renderer.rb +22 -16
  115. data/lib/hecks/forms.rb +4 -0
  116. data/lib/hecks/fqn.rb +21 -9
  117. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +30 -8
  118. data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -1
  119. data/lib/hecks/fuzzing/isolated_boot.rb +14 -6
  120. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +264 -0
  121. data/lib/hecks/fuzzing/properties/guards.rb +251 -0
  122. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +351 -0
  123. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +99 -0
  124. data/lib/hecks/fuzzing/properties/querying.rb +206 -0
  125. data/lib/hecks/fuzzing/properties.rb +41 -1065
  126. data/lib/hecks/fuzzing/replay.rb +60 -16
  127. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +6 -2
  128. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +2 -1
  129. data/lib/hecks/fuzzing/sequence_generator.rb +0 -1
  130. data/lib/hecks/fuzzing/value_generator.rb +2 -2
  131. data/lib/hecks/grammar/evolve.rb +34 -10
  132. data/lib/hecks/grammar/expression_operators.json +124 -0
  133. data/lib/hecks/grammar.rb +15 -5
  134. data/lib/hecks/ir.rb +9 -0
  135. data/lib/hecks/language/bluebook/aggregate.bluebook +4 -2
  136. data/lib/hecks/language/bluebook/command.bluebook +2 -1
  137. data/lib/hecks/language/bluebook/entity.bluebook +2 -1
  138. data/lib/hecks/language/bluebook/projection.bluebook +2 -1
  139. data/lib/hecks/language/bluebook/query.bluebook +4 -2
  140. data/lib/hecks/language/bluebook/shape.bluebook +2 -1
  141. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -0
  142. data/lib/hecks/literal.rb +18 -8
  143. data/lib/hecks/naming.rb +6 -3
  144. data/lib/hecks/ports/access_control.rb +1 -1
  145. data/lib/hecks/ports/agent.rb +5 -3
  146. data/lib/hecks/ports/authentication.rb +3 -2
  147. data/lib/hecks/ports/authorization.rb +1 -1
  148. data/lib/hecks/ports/clock.rb +1 -1
  149. data/lib/hecks/ports/extraction.rb +5 -1
  150. data/lib/hecks/ports/identity_assignment.rb +1 -1
  151. data/lib/hecks/ports/identity_generation.rb +1 -1
  152. data/lib/hecks/ports/identity_resolution.rb +1 -1
  153. data/lib/hecks/ports/loading.rb +4 -1
  154. data/lib/hecks/ports/persistence/append_only.rb +51 -4
  155. data/lib/hecks/ports/persistence/binding_policy.rb +1 -1
  156. data/lib/hecks/ports/persistence/plugin.rb +3 -0
  157. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -1
  158. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +5 -5
  159. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +14 -5
  160. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +4 -0
  161. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +1 -1
  162. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +80 -16
  163. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +23 -6
  164. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +43 -5
  165. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +25 -4
  166. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +49 -9
  167. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +2 -2
  168. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +10 -0
  169. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +49 -38
  170. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +77 -18
  171. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +19 -21
  172. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +27 -9
  173. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +9 -0
  174. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +25 -10
  175. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +2 -6
  176. data/lib/hecks/ports/persistence/plugins/era.rb +3 -1
  177. data/lib/hecks/ports/persistence/remote_runtime.rb +1 -1
  178. data/lib/hecks/ports/persistence.rb +7 -3
  179. data/lib/hecks/ports/projection.rb +13 -5
  180. data/lib/hecks/ports/query/in_memory.rb +8 -2
  181. data/lib/hecks/ports/query.rb +1 -3
  182. data/lib/hecks/ports.rb +2 -0
  183. data/lib/hecks/projections/diagrams.rb +11 -3
  184. data/lib/hecks/projections/glossary.rb +374 -0
  185. data/lib/hecks/projections/ir.rb +2 -0
  186. data/lib/hecks/projections/model/deviations.rb +2 -1
  187. data/lib/hecks/projections/model.rb +15 -10
  188. data/lib/hecks/projections/vocabulary.rb +1 -3
  189. data/lib/hecks/projections.rb +1 -0
  190. data/lib/hecks/projector/cli_projector.rb +78 -42
  191. data/lib/hecks/projector/docs_projector.rb +21 -12
  192. data/lib/hecks/projector/exporter.rb +34 -1
  193. data/lib/hecks/projector/narrate_projector.rb +83 -25
  194. data/lib/hecks/projector.rb +2 -2
  195. data/lib/hecks/query_ir.rb +60 -36
  196. data/lib/hecks/query_specification/common/authorization_spec.rb +1 -1
  197. data/lib/hecks/query_specification/common/comparators.rb +4 -0
  198. data/lib/hecks/query_specification/common/comparison.rb +8 -1
  199. data/lib/hecks/query_specification/common/dsl.rb +5 -0
  200. data/lib/hecks/query_specification/common/limit_spec.rb +9 -2
  201. data/lib/hecks/query_specification/common/null_policy.rb +6 -2
  202. data/lib/hecks/query_specification/common/offset_spec.rb +9 -2
  203. data/lib/hecks/query_specification/common/options.rb +17 -5
  204. data/lib/hecks/query_specification/common/order_by.rb +9 -2
  205. data/lib/hecks/query_specification/common/where_clause.rb +15 -2
  206. data/lib/hecks/query_specification/field_path.rb +4 -4
  207. data/lib/hecks/query_specification/read_model/specification.rb +5 -2
  208. data/lib/hecks/router/namespace_installer.rb +22 -10
  209. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +57 -22
  210. data/lib/hecks/runtime/command_interpreter.rb +104 -65
  211. data/lib/hecks/runtime/command_rules/admissibility.rb +26 -8
  212. data/lib/hecks/runtime/command_rules/arithmetic.rb +35 -14
  213. data/lib/hecks/runtime/dependency_planning.rb +33 -21
  214. data/lib/hecks/runtime/dispatcher.rb +41 -7
  215. data/lib/hecks/runtime/entity_element.rb +25 -6
  216. data/lib/hecks/runtime/entity_interpreter.rb +28 -15
  217. data/lib/hecks/runtime/errors.rb +28 -2
  218. data/lib/hecks/runtime/instance.rb +9 -3
  219. data/lib/hecks/runtime/interpreting.rb +45 -9
  220. data/lib/hecks/runtime/loader.rb +21 -0
  221. data/lib/hecks/runtime/outbox.rb +365 -0
  222. data/lib/hecks/runtime/policy_interpreter.rb +32 -17
  223. data/lib/hecks/runtime/query_interpreter.rb +36 -16
  224. data/lib/hecks/runtime/reaction_invocation.rb +35 -22
  225. data/lib/hecks/runtime/read_model_interpreter.rb +31 -7
  226. data/lib/hecks/runtime/reference_hop.rb +1 -1
  227. data/lib/hecks/runtime/refusal_wording.rb +55 -49
  228. data/lib/hecks/runtime/registry/saga_persistence.rb +3 -1
  229. data/lib/hecks/runtime/registry/verification.rb +39 -2
  230. data/lib/hecks/runtime/registry.rb +21 -11
  231. data/lib/hecks/runtime/remote_dispatcher.rb +3 -1
  232. data/lib/hecks/runtime/routing.rb +52 -39
  233. data/lib/hecks/runtime/saga_interpreter/correlation.rb +7 -7
  234. data/lib/hecks/runtime/saga_interpreter.rb +119 -90
  235. data/lib/hecks/runtime/saga_pending_dispatch.rb +6 -3
  236. data/lib/hecks/runtime/tenant_scope.rb +7 -0
  237. data/lib/hecks/runtime/value/coercion.rb +271 -49
  238. data/lib/hecks/runtime/value.rb +1 -1
  239. data/lib/hecks/runtime.rb +7 -2
  240. data/lib/hecks/storehouse.rb +15 -8
  241. data/lib/hecks/version.rb +1 -1
  242. data/lib/hecks/vocabulary.rb +1 -0
  243. data/lib/hecks.rb +17 -13
  244. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +1 -1
  245. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +1 -1
  246. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +1 -1
  247. metadata +16 -2
@@ -2,6 +2,12 @@ module Hecks
2
2
  module Adapters
3
3
  class PostgresEra
4
4
  class Lineage
5
+ # Owner-only schema bootstrap: creates the lineage tables
6
+ # (`hecks_eras`, `hecks_era_texts`, `hecks_approvals`, the
7
+ # partitioned journal), sets up its RLS fence, bridges a renamed
8
+ # domain's history (`rename_domain!`), and attaches each era's
9
+ # journal partition (`ensure_partition!`) — the build-then-ATTACH
10
+ # dance that keeps a mint from stopping every writer.
5
11
  module Provisioning
6
12
  # Provisioning is the OWNER's job, and a deployment's app role is
7
13
  # deliberately not the owner — it may append and read, and it
@@ -13,6 +19,15 @@ module Hecks
13
19
  # grant_era! exists to constrain could never finish booting
14
20
  # ("must be owner of table hecks_eras"). A fence nothing can
15
21
  # reach is not a fence.
22
+ # One bootstrap sequence of DDL, order-dependent throughout —
23
+ # every guarded ALTER (RLS, REVOKE) is guarded and placed
24
+ # exactly where it is for measured, documented reasons (see the
25
+ # inline comments on each: catalog-lock avoidance, RLS timing,
26
+ # FORCE semantics). Splitting this into smaller methods would
27
+ # only hide that single ordered sequence behind several call
28
+ # sites, with nothing gained — each statement already reads as
29
+ # one step, and the length here is DDL, not branching logic.
30
+ # rubocop:disable-next Metrics/MethodLength
16
31
  def ensure_base!
17
32
  rename_domain! if @formerly_known_as
18
33
  return unless provisioner?
@@ -183,6 +198,16 @@ module Hecks
183
198
  # Anything else (no rows under either name) falls through
184
199
  # harmlessly — `formerly_known_as` pointing at a name with no
185
200
  # held history is not an error, just inert.
201
+ # One transactional migration, whose own comment above spells
202
+ # out a three-way precheck and a FIXED lock-acquisition order
203
+ # (old before new, eras before ordinal) chosen specifically to
204
+ # avoid a deadlock against a concurrent rename/mint/write.
205
+ # Splitting this would separate the precheck from the locking
206
+ # from the renames from the commit/rollback handling — each a
207
+ # piece of ONE transaction that must stay in this exact order
208
+ # or the deadlock-avoidance guarantee stops holding.
209
+ # rubocop:disable-next Metrics/AbcSize
210
+ # rubocop:disable-next Metrics/MethodLength
186
211
  def rename_domain!
187
212
  return unless @db.exec_params("SELECT to_regclass($1)", ["hecks_eras"])[0]["to_regclass"]
188
213
  return if @db.exec_params(
@@ -236,17 +261,30 @@ module Hecks
236
261
  # ensure_base! at all — only lazily, on first reattest! — so a
237
262
  # domain that never needed one must not be forced through an
238
263
  # UPDATE against a table that doesn't exist.
239
- @db.exec_params("UPDATE hecks_attestations SET domain = $1 WHERE domain = $2", [@domain, @formerly_known_as]) if @db.exec_params("SELECT to_regclass($1)", ["hecks_attestations"])[0]["to_regclass"]
264
+ if @db.exec_params(
265
+ "SELECT to_regclass($1)", ["hecks_attestations"]
266
+ )[0]["to_regclass"]
267
+ @db.exec_params("UPDATE hecks_attestations SET domain = $1 WHERE domain = $2",
268
+ [@domain, @formerly_known_as])
269
+ end
240
270
 
241
271
  @db.exec("COMMIT")
242
272
  rescue PG::LockNotAvailable
243
- @db.exec("ROLLBACK") rescue nil
273
+ begin
274
+ @db.exec("ROLLBACK")
275
+ rescue StandardError
276
+ nil
277
+ end
244
278
  raise Runtime::WiringError,
245
279
  "cannot rename #{@formerly_known_as} to #{@domain}: another rename, mint, or write holds " \
246
280
  "one of the domain locks — waited 10s; try again shortly"
247
- rescue PG::Error => error
248
- @db.exec("ROLLBACK") rescue nil
249
- raise Runtime::WiringError, "cannot rename #{@formerly_known_as} to #{@domain}: #{error.message.strip}"
281
+ rescue PG::Error => e
282
+ begin
283
+ @db.exec("ROLLBACK")
284
+ rescue StandardError
285
+ nil
286
+ end
287
+ raise Runtime::WiringError, "cannot rename #{@formerly_known_as} to #{@domain}: #{e.message.strip}"
250
288
  end
251
289
 
252
290
  # Nothing provisioned yet — build it. Provisioned and owned —
@@ -4,6 +4,10 @@ module Hecks
4
4
  module Adapters
5
5
  class PostgresEra
6
6
  class Lineage
7
+ # The one deliberate, human-driven command that closes a fork:
8
+ # `merge_tail!` re-enters an old era's post-cut writes into the
9
+ # current era under named winners, and `diverged_count` observes how
10
+ # far the two worlds have drifted at any time before that.
7
11
  module TailMerge
8
12
  # ── fork observability ─────────────────────────────────────────
9
13
 
@@ -29,6 +33,14 @@ module Hecks
29
33
  # winner; resolution itself is append-only (the winner's state
30
34
  # re-enters as the newest row and wins structurally — originals
31
35
  # stay immutable).
36
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
37
+ # One Postgres transaction (BEGIN…COMMIT) with a manual ROLLBACK
38
+ # at every early refusal, and a snapshot-before-mutation
39
+ # invariant (`new_states` captured before the head rebuild lets
40
+ # the tail interleave). Splitting the steps into separate
41
+ # methods would force each to independently know how to roll
42
+ # back the same shared transaction, and would separate the
43
+ # snapshot from the mutation it must precede.
32
44
  def merge_tail!(aggregates:, edges:, winners: {}, audit: nil)
33
45
  @db.exec("BEGIN")
34
46
  @db.exec("SET LOCAL lock_timeout = '10s'")
@@ -129,14 +141,23 @@ module Hecks
129
141
  @db.exec("COMMIT")
130
142
  true
131
143
  rescue PG::LockNotAvailable
132
- @db.exec("ROLLBACK") rescue nil
144
+ begin
145
+ @db.exec("ROLLBACK")
146
+ rescue StandardError
147
+ nil
148
+ end
133
149
  raise Runtime::WiringError,
134
150
  "cannot merge the tail of #{@domain}: another mint or merge holds the domain lock — " \
135
151
  "waited 10s; try again shortly"
136
- rescue PG::Error => error
137
- @db.exec("ROLLBACK") rescue nil
138
- raise Runtime::WiringError, "cannot merge the tail of #{@domain}: #{error.message.strip}"
152
+ rescue PG::Error => e
153
+ begin
154
+ @db.exec("ROLLBACK")
155
+ rescue StandardError
156
+ nil
157
+ end
158
+ raise Runtime::WiringError, "cannot merge the tail of #{@domain}: #{e.message.strip}"
139
159
  end
160
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
140
161
 
141
162
  # Ids touched by BOTH worlds since the cut — the old world's
142
163
  # post-cut tail INTERSECTed with the new world's own writes.
@@ -2,6 +2,12 @@ module Hecks
2
2
  module Adapters
3
3
  class PostgresEra
4
4
  class Lineage
5
+ # Installs the shared, domain-independent `hecks_tr_*` Postgres
6
+ # functions (extract/insert/rename/move/convert/drop over jsonb)
7
+ # that a compiled translation rule set's SQL calls into — the SQL
8
+ # compilation target kept equal to the Ruby reference transform by
9
+ # the cross-execution equivalence spec, never a second source of
10
+ # truth.
5
11
  module TransformInstaller
6
12
  # The jsonb rule transforms — installed once, idempotently. Kept
7
13
  # equal to the port's reference entry-JSON transform by the
@@ -27,7 +33,26 @@ module Hecks
27
33
  end
28
34
  end
29
35
 
36
+ # Six independent CREATE OR REPLACE FUNCTION statements — each
37
+ # self-contained SQL, no shared Ruby state, and (per this
38
+ # module's own header comment) safe in any install order since
39
+ # plpgsql bodies aren't resolved against each other until
40
+ # called, not at CREATE time. Split one-per-function below
41
+ # purely so each has its own name and (where relevant) its own
42
+ # comment to sit next to, not because the six have any
43
+ # sequencing dependency on one another.
30
44
  def install_transform_functions!
45
+ install_hecks_tr_extract!
46
+ install_hecks_tr_insert!
47
+ install_hecks_tr_rename!
48
+ install_hecks_tr_move!
49
+ install_hecks_tr_convert!
50
+ install_hecks_tr_drop!
51
+ end
52
+
53
+ private
54
+
55
+ def install_hecks_tr_extract!
31
56
  @db.exec(<<~SQL)
32
57
  CREATE OR REPLACE FUNCTION hecks_tr_extract(state jsonb, path text[], OUT remaining jsonb, OUT value jsonb, OUT present boolean)
33
58
  LANGUAGE plpgsql IMMUTABLE AS $fn$
@@ -57,15 +82,18 @@ module Hecks
57
82
  END IF;
58
83
  END $fn$
59
84
  SQL
60
- # ADVERSARIAL FINDING: a destination whose top segment already
61
- # holds a value — most commonly a reference, a bare scalar id
62
- # used to be silently overwritten with an empty object the
63
- # moment a dotted destination needed to nest under it. That is
64
- # a drop that never declared itself, the one thing this
65
- # language exists to make explicit (see hecks_tr_convert's own
66
- # refusal below, the same shape) refused here instead, with
67
- # the Ruby reference transform (ports/persistence/lineage.rb's
68
- # `insert`) raising the identical wording.
85
+ end
86
+
87
+ # ADVERSARIAL FINDING: a destination whose top segment already
88
+ # holds a value most commonly a reference, a bare scalar id
89
+ # used to be silently overwritten with an empty object the
90
+ # moment a dotted destination needed to nest under it. That is
91
+ # a drop that never declared itself, the one thing this
92
+ # language exists to make explicit (see hecks_tr_convert's own
93
+ # refusal below, the same shape) — refused here instead, with
94
+ # the Ruby reference transform (ports/persistence/lineage.rb's
95
+ # `insert`) raising the identical wording.
96
+ def install_hecks_tr_insert!
69
97
  @db.exec(<<~SQL)
70
98
  CREATE OR REPLACE FUNCTION hecks_tr_insert(state jsonb, path text[], value jsonb, rule_label text) RETURNS jsonb
71
99
  LANGUAGE plpgsql IMMUTABLE AS $fn$
@@ -83,6 +111,9 @@ module Hecks
83
111
  RETURN jsonb_set(state, path, value);
84
112
  END $fn$
85
113
  SQL
114
+ end
115
+
116
+ def install_hecks_tr_rename!
86
117
  @db.exec(<<~SQL)
87
118
  CREATE OR REPLACE FUNCTION hecks_tr_rename(state jsonb, old_name text, new_name text) RETURNS jsonb
88
119
  LANGUAGE sql IMMUTABLE AS $fn$
@@ -91,6 +122,9 @@ module Hecks
91
122
  ELSE state END
92
123
  $fn$
93
124
  SQL
125
+ end
126
+
127
+ def install_hecks_tr_move!
94
128
  @db.exec(<<~SQL)
95
129
  CREATE OR REPLACE FUNCTION hecks_tr_move(state jsonb, from_path text[], to_path text[], rule_label text) RETURNS jsonb
96
130
  LANGUAGE plpgsql IMMUTABLE AS $fn$
@@ -101,6 +135,9 @@ module Hecks
101
135
  RETURN hecks_tr_insert(extracted.remaining, to_path, extracted.value, rule_label);
102
136
  END $fn$
103
137
  SQL
138
+ end
139
+
140
+ def install_hecks_tr_convert!
104
141
  @db.exec(<<~SQL)
105
142
  CREATE OR REPLACE FUNCTION hecks_tr_convert(state jsonb, from_path text[], to_path text[], pairs jsonb, from_label text, rule_label text) RETURNS jsonb
106
143
  LANGUAGE plpgsql IMMUTABLE AS $fn$
@@ -117,6 +154,9 @@ module Hecks
117
154
  from_label, extracted.value, extracted.value;
118
155
  END $fn$
119
156
  SQL
157
+ end
158
+
159
+ def install_hecks_tr_drop!
120
160
  @db.exec(<<~SQL)
121
161
  CREATE OR REPLACE FUNCTION hecks_tr_drop(state jsonb, path text[]) RETURNS jsonb
122
162
  LANGUAGE plpgsql IMMUTABLE AS $fn$
@@ -66,8 +66,8 @@ module Hecks
66
66
  declared = edge.for_aggregate(aggregate.name)
67
67
  after = begin
68
68
  lineage.translated_latest(aggregate, ordinal, chain)
69
- rescue PG::Error => error
70
- raise Runtime::WiringError, "cannot mint era #{ordinal} of #{bluebook.name}: #{error.message.strip}"
69
+ rescue PG::Error => e
70
+ raise Runtime::WiringError, "cannot mint era #{ordinal} of #{bluebook.name}: #{e.message.strip}"
71
71
  end
72
72
  before = if chain.size > 1
73
73
  lineage.translated_latest(aggregate, ordinal, chain[0..-2])
@@ -10,6 +10,15 @@ module Hecks
10
10
  # held-but-superseded shape boots read-only-toward-the-fence; an
11
11
  # unheld shape goes to the minter.
12
12
  module EraResolver
13
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
14
+ # A single order-dependent dispatch over a closed set of four
15
+ # boot scenarios (first boot / quiet reboot / held-but-superseded
16
+ # / unheld-shape-goes-to-minter), each already explained by its
17
+ # own comment above, all sharing one `db` connection closed in
18
+ # `ensure`. Splitting the branches into separate methods would
19
+ # turn each `return` (which exits `check!`, closing `db`) into a
20
+ # sentinel value threaded back up, obscuring the mutual
21
+ # exclusivity that IS the method's whole point.
13
22
  def check!(registry:, bluebook:, current_text:, settings:, directory: nil)
14
23
  db = PostgresEra.connect_for(bluebook.name, settings)
15
24
  lineage = Lineage.new(db, bluebook.name, formerly_known_as: bluebook.formerly_known_as)
@@ -78,6 +87,7 @@ module Hecks
78
87
  ensure
79
88
  db&.close
80
89
  end
90
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
81
91
  end
82
92
  end
83
93
  end
@@ -20,10 +20,29 @@ module Hecks
20
20
  label = hash[0, Runtime::StorageShape::LABEL_LENGTH]
21
21
  ordinal = latest[:ordinal] + 1
22
22
 
23
+ edge = resolve_edge!(registry, bluebook, lineage, latest, label, ordinal, directory)
24
+ ensure_compute_rekey_approved!(bluebook, lineage, edge, ordinal)
25
+ check_coverage!(registry, bluebook, shadow(latest[:held_text]), edge)
26
+
27
+ chain = edge_chain(registry, bluebook, lineage.eras, label)
28
+ audit!(bluebook, lineage, chain, ordinal, edge)
29
+ lineage.mint_era!(
30
+ ordinal: ordinal, hash: hash, label: label, held_text: current_text,
31
+ aggregates: bluebook.aggregates, edges: chain, role: role,
32
+ projection: Runtime::StorageShape.project(bluebook)
33
+ )
34
+ ordinal
35
+ end
36
+
37
+ # Find the one translation edge that leaves the held era, and
38
+ # validate it: exactly one edge must leave (eras fork
39
+ # mechanically — a second edge from the same source is a wiring
40
+ # mistake, not a merge to resolve automatically), and it must
41
+ # target the current shape's label (otherwise the edge is stale
42
+ # and boot must not silently mint past it).
43
+ def resolve_edge!(registry, bluebook, lineage, latest, label, ordinal, directory)
23
44
  edges = registry.translations.select { |t| t.domain == bluebook.name && t.from == latest[:label] }
24
- if edges.empty?
25
- refuse_toward_the_scaffold!(registry, bluebook, lineage, latest, ordinal, directory)
26
- end
45
+ refuse_toward_the_scaffold!(registry, bluebook, lineage, latest, ordinal, directory) if edges.empty?
27
46
  if edges.size > 1
28
47
  raise Runtime::WiringError,
29
48
  "cannot boot #{bluebook.name}: #{edges.size} translation edges leave era #{latest[:ordinal]} " \
@@ -36,43 +55,35 @@ module Hecks
36
55
  "cannot boot #{bluebook.name}: the translation edge from #{latest[:label]} targets " \
37
56
  "#{edge.to}, but the current shape is #{label} — the edge is stale; re-run bin/scaffold_translation"
38
57
  end
58
+ edge
59
+ end
39
60
 
40
- # A compute's (and, the same way, a rekey's) only verification
41
- # is the audit's human-approved sample — mint stays
42
- # non-interactive by requiring the approval to already exist,
43
- # recorded IN THIS DATABASE by `bin/translation_audit …
44
- # --approve` and bound to what was actually reviewed: this
45
- # edge's parsed content, and the journal as it stood when the
46
- # samples were read. A journal that has advanced past the
47
- # review invalidates it — the approved samples no longer cover
48
- # the data.
49
- if edge.aggregates.any? { |declared| !declared.computes.empty? || !declared.rekeys.empty? }
50
- approval = lineage.approval_for(from: edge.from, to: edge.to)
51
- unless approval && approval[:edge_digest] == Translation::Audit.edge_digest(edge)
52
- raise Runtime::WiringError,
53
- "cannot mint era #{ordinal} of #{bluebook.name}: this edge carries a compute or rekey " \
54
- "rule, and the audit's human-approved sample is its only verification — run " \
55
- "bin/translation_audit with --approve, then boot again"
56
- end
57
- tip = lineage.last_ordinal
58
- if approval[:reviewed_ordinal] != tip
59
- raise Runtime::WiringError,
60
- "cannot mint era #{ordinal} of #{bluebook.name}: the journal advanced past the approved " \
61
- "review (ordinal #{approval[:reviewed_ordinal]} reviewed, #{tip} now) — the samples a " \
62
- "human approved no longer cover the data; re-run bin/translation_audit with --approve"
63
- end
64
- end
61
+ # A compute's (and, the same way, a rekey's) only verification
62
+ # is the audit's human-approved sample — mint stays
63
+ # non-interactive by requiring the approval to already exist,
64
+ # recorded IN THIS DATABASE by `bin/translation_audit …
65
+ # --approve` and bound to what was actually reviewed: this
66
+ # edge's parsed content, and the journal as it stood when the
67
+ # samples were read. A journal that has advanced past the
68
+ # review invalidates it — the approved samples no longer cover
69
+ # the data.
70
+ def ensure_compute_rekey_approved!(bluebook, lineage, edge, ordinal)
71
+ return unless edge.aggregates.any? { |declared| !declared.computes.empty? || !declared.rekeys.empty? }
65
72
 
66
- check_coverage!(registry, bluebook, shadow(latest[:held_text]), edge)
73
+ approval = lineage.approval_for(from: edge.from, to: edge.to)
74
+ unless approval && approval[:edge_digest] == Translation::Audit.edge_digest(edge)
75
+ raise Runtime::WiringError,
76
+ "cannot mint era #{ordinal} of #{bluebook.name}: this edge carries a compute or rekey " \
77
+ "rule, and the audit's human-approved sample is its only verification — run " \
78
+ "bin/translation_audit with --approve, then boot again"
79
+ end
80
+ tip = lineage.last_ordinal
81
+ return unless approval[:reviewed_ordinal] != tip
67
82
 
68
- chain = edge_chain(registry, bluebook, lineage.eras, label)
69
- audit!(bluebook, lineage, chain, ordinal, edge)
70
- lineage.mint_era!(
71
- ordinal: ordinal, hash: hash, label: label, held_text: current_text,
72
- aggregates: bluebook.aggregates, edges: chain, role: role,
73
- projection: Runtime::StorageShape.project(bluebook)
74
- )
75
- ordinal
83
+ raise Runtime::WiringError,
84
+ "cannot mint era #{ordinal} of #{bluebook.name}: the journal advanced past the approved " \
85
+ "review (ordinal #{approval[:reviewed_ordinal]} reviewed, #{tip} now) — the samples a " \
86
+ "human approved no longer cover the data; re-run bin/translation_audit with --approve"
76
87
  end
77
88
 
78
89
  # No edge yet: the boot refuses toward the authoring loop —
@@ -96,7 +107,7 @@ module Hecks
96
107
  # Diff the held era against the current shape and write the edge
97
108
  # file — confident rules inline, ambiguities as parse-refusing
98
109
  # `unresolved` lines. Returns the file path.
99
- def scaffold!(registry, bluebook, lineage, latest, directory)
110
+ def scaffold!(_registry, bluebook, lineage, latest, directory)
100
111
  ensure_named!(lineage, latest)
101
112
  latest = lineage.eras.last
102
113
 
@@ -1,6 +1,8 @@
1
1
  require "json"
2
2
 
3
3
  require_relative "../../../../adapters/driven/sql_query_builder"
4
+ require_relative "../../../../adapters/driven/postgres/outbox"
5
+ require_relative "../../../../adapters/driven/postgres/reconnect"
4
6
  require_relative "postgres_era/lineage"
5
7
  require_relative "postgres_era/lineage_manager"
6
8
  require_relative "../../../../ports/persistence/append_only"
@@ -40,10 +42,20 @@ module Hecks
40
42
  # operator compiles fully into SQL, or the query refuses loudly.
41
43
  class PostgresEra
42
44
  include SqlQueryBuilder
45
+ include Adapters::PostgresOutbox
46
+ include Adapters::PostgresReconnect
43
47
 
44
48
  attr_reader :aggregate
45
49
 
46
- def persistence_capabilities = [:atomic_put]
50
+ # `:cross_process_lock` tells `Interpreting#run_dispatch_order_with_isolation`
51
+ # (runtime/interpreting.rb) this repository can hold a REAL
52
+ # cross-process lock for the whole dispatch order itself, via
53
+ # `with_write_lock` below — so it should use that instead of the
54
+ # in-process `AggregateLock` `Mutex` every other non-CAS repository
55
+ # falls back to. See ADR 0036: that in-process Mutex is invisible
56
+ # to `rust/host` dispatching against the same PostgresEra-bound
57
+ # tables from a separate OS process.
58
+ def persistence_capabilities = %i[atomic_put cross_process_lock]
47
59
 
48
60
  # The capability idiom: only PostgresEra answers true, and only
49
61
  # PostgresEra carries an era_check! for the boot gate to delegate to.
@@ -139,18 +151,42 @@ module Hecks
139
151
  # problems) still surface.
140
152
  connection.exec("SET client_min_messages = warning")
141
153
  connection
142
- rescue PG::Error => error
154
+ rescue PG::Error => e
143
155
  raise Runtime::WiringError,
144
- "cannot bind PostgresEra at #{declared} for #{name}: #{error.message.strip}"
156
+ "cannot bind PostgresEra at #{declared} for #{name}: #{e.message.strip}"
145
157
  end
146
158
 
147
159
  def initialize(aggregate:, settings: {}, root: nil)
148
160
  @aggregate = aggregate
161
+ @settings = settings
149
162
  @db = self.class.connect_for(aggregate.name, settings)
150
163
  # The domain names the journal (one journal per lineage). The
151
- # factory injects it; a directly-instantiated adapter (specs,
152
- # consoles) journals under the aggregate's own name.
153
- @domain = self.class.setting(settings, :domain, default: aggregate.storage_name).to_s
164
+ # factory injects it as the owning bluebook's own declared name
165
+ # (`RepositoryFactory.build`'s own `domain` positional arg
166
+ # `bluebook.name`, confirmed at `outbox.rb`'s own call site —
167
+ # never this default in real dispatch). This default exists only
168
+ # for a directly-instantiated adapter (specs, consoles) that
169
+ # skips the factory.
170
+ #
171
+ # When the aggregate DOES have an owning chapter (`hecks_owner`
172
+ # set — true for any aggregate sealed through a real bluebook,
173
+ # even if this adapter itself was built by hand), default to the
174
+ # chapter's own declared PascalCase name. That's the same string
175
+ # `rust/host`'s `pg_advisory_xact_lock` key derives from
176
+ # (`HECKS_DOMAIN`, deploy-time-set to `world.domain`) — the
177
+ # aggregate's own snake_case `storage_name` silently disagreed
178
+ # with it (`hashtext` is byte-sensitive: `"chess"` and `"Chess"`
179
+ # never collide), which is ADR 0036 Blocker 1.
180
+ #
181
+ # When there's no owning chapter at all (a bare
182
+ # `AggregateBuilder` fixture built outside any real bluebook —
183
+ # `hecks_owner` is only ever stamped by chapter construction, see
184
+ # `traits.rb`'s `hecks_owner = self`) there is no chapter name to
185
+ # match Rust against in the first place, so fall back to the
186
+ # aggregate's own name, same as before this ADR.
187
+ @domain = self.class.setting(
188
+ settings, :domain, default: aggregate.hecks_owner&.name || aggregate.storage_name
189
+ ).to_s
154
190
  @lineage = Lineage.new(@db, @domain)
155
191
  @lineage.ensure_base!
156
192
  # The era gate resolves which era this boot IS (an old checkout
@@ -192,6 +228,7 @@ module Hecks
192
228
  @field_caches = ensure_field_caches!
193
229
  create_event_table!
194
230
  create_saga_table!
231
+ create_outbox_table!
195
232
  end
196
233
 
197
234
  def table = @aggregate.storage_name
@@ -216,7 +253,10 @@ module Hecks
216
253
  return @db.exec(%(SELECT id, state FROM #{quoted_head} ORDER BY id)).map { |row| instance(row) } unless order_by
217
254
 
218
255
  name = order_by.to_s.split(".").first
219
- raise Runtime::WiringError, "#{@aggregate.name} has no attribute #{order_by.inspect} to order by" unless @aggregate.lifecycle&.field.to_s == name || @aggregate.attribute(name)
256
+ unless @aggregate.lifecycle&.field.to_s == name || @aggregate.attribute(name)
257
+ raise Runtime::WiringError,
258
+ "#{@aggregate.name} has no attribute #{order_by.inspect} to order by"
259
+ end
220
260
 
221
261
  spec = QuerySpecification::Common::OrderBy.new(field: order_by, direction: direction)
222
262
  @db.exec(%(SELECT id, state FROM #{quoted_head} ORDER BY #{order_clause(spec, nil)})).map { |row| instance(row) }
@@ -268,6 +308,18 @@ module Hecks
268
308
  head_phase(declared, uncached, ids, args)
269
309
  end
270
310
 
311
+ # ADR 0036's actual fix — see `persistence_capabilities` above.
312
+ # Wraps the WHOLE dispatch order (hydrate through save), not just
313
+ # `append`'s own transaction below: `lock_writes!` has to be held
314
+ # before hydrate even starts, or two cross-process writers can
315
+ # both hydrate unlocked and race for the write, each blind to the
316
+ # other. `transaction` (via `include Adapters::PostgresOutbox`) is
317
+ # already re-entrant — `append`/`atomic_put`'s own inner
318
+ # `transaction do ... end`, deep inside the block below, joins
319
+ # this SAME transaction instead of opening/committing its own, so
320
+ # the advisory lock stays held until this whole block returns.
321
+ def with_write_lock(&block) = transaction { lock_writes!; block.call } # rubocop:disable Style/Semicolon
322
+
271
323
  # HELD FOR THE WHOLE TRANSACTION, not just around the INSERT — the
272
324
  # ordinal is assigned by the column's own `nextval()` default, inside
273
325
  # this same statement, so the lock has to already be held before that
@@ -284,9 +336,12 @@ module Hecks
284
336
  # still runs, cheaply, during AppendOnly#recover!'s full replay on
285
337
  # every boot (see `project` below), and a second write there would
286
338
  # make that replay pay real DB cost for a snapshot that's already
287
- # correct.
339
+ # correct. When `with_write_lock` above already drove this whole
340
+ # dispatch, `lock_writes!` here is a harmless re-acquire of the
341
+ # same already-held (per-session-reentrant) advisory lock; a bare
342
+ # `repository.save` outside a full dispatch still takes it fresh.
288
343
  def append(entry)
289
- @db.transaction do
344
+ transaction do
290
345
  lock_writes!
291
346
  append_and_project!(entry)
292
347
  end
@@ -299,7 +354,7 @@ module Hecks
299
354
  # occurs before entering this adapter-native operation.
300
355
  def atomic_put(entry, insert_only: false)
301
356
  status = nil
302
- @db.transaction do
357
+ transaction do
303
358
  lock_writes!
304
359
  exists = !@db.exec_params(
305
360
  "SELECT 1 FROM #{quoted_head} WHERE id = $1 LIMIT 1",
@@ -418,7 +473,8 @@ module Hecks
418
473
  "ON CONFLICT (domain, process_manager, correlation) DO UPDATE " \
419
474
  "SET state = EXCLUDED.state, memory = EXCLUDED.memory, " \
420
475
  "completed_compensations = EXCLUDED.completed_compensations, updated_at = now()",
421
- [@domain, process_manager.to_s, correlation.to_s, state.to_s, JSON.generate(memory), JSON.generate(completed_compensations)]
476
+ [@domain, process_manager.to_s, correlation.to_s, state.to_s, JSON.generate(memory),
477
+ JSON.generate(completed_compensations)]
422
478
  )
423
479
  end
424
480
 
@@ -433,7 +489,8 @@ module Hecks
433
489
  return enum_for(:each_saga) unless block_given?
434
490
 
435
491
  @db.exec_params(
436
- "SELECT process_manager, correlation, state, memory, completed_compensations FROM hecks_saga_instances WHERE domain = $1",
492
+ "SELECT process_manager, correlation, state, memory, completed_compensations " \
493
+ "FROM hecks_saga_instances WHERE domain = $1",
437
494
  [@domain]
438
495
  ).each do |row|
439
496
  yield row["process_manager"], row["correlation"], row["state"],
@@ -624,11 +681,12 @@ module Hecks
624
681
  end
625
682
 
626
683
  def cached_where_fields
627
- declared_queries.flat_map { |q|
628
- q.wheres.map { |clause|
684
+ fields = declared_queries.flat_map do |q|
685
+ q.wheres.map do |clause|
629
686
  clause.field.to_s
630
- }
631
- }.uniq.select { |field| cacheable_field?(field) }
687
+ end
688
+ end
689
+ fields.uniq.select { |field| cacheable_field?(field) }
632
690
  end
633
691
 
634
692
  def declared_queries
@@ -709,7 +767,7 @@ module Hecks
709
767
  clauses << where_clause(clause.op.to_s, expression, value, binds, field: clause.field)
710
768
  end
711
769
 
712
- sql = +"SELECT #{select_list} FROM #{from_relation} WHERE #{clauses.join(' AND ')}"
770
+ sql = "SELECT #{select_list} FROM #{from_relation} WHERE #{clauses.join(' AND ')}"
713
771
  sql << if declared.order_by
714
772
  " ORDER BY #{order_clause(declared.order_by, declared.null_semantics)}"
715
773
  else
@@ -751,7 +809,8 @@ module Hecks
751
809
  # this same domain already created before this column existed —
752
810
  # the same idiom `rust/host/src/journal.rs`'s own
753
811
  # `sagas_backfilled` column addition already uses.
754
- @db.exec("ALTER TABLE hecks_saga_instances ADD COLUMN IF NOT EXISTS completed_compensations jsonb NOT NULL DEFAULT '[]'::jsonb")
812
+ @db.exec("ALTER TABLE hecks_saga_instances ADD COLUMN IF NOT EXISTS completed_compensations jsonb " \
813
+ "NOT NULL DEFAULT '[]'::jsonb")
755
814
  end
756
815
  end
757
816
  end
@@ -15,29 +15,27 @@ module Hecks
15
15
  module LayerOne
16
16
  def layer_one!(violations, aggregate, after)
17
17
  after.each do |id, state|
18
- begin
19
- symbolized = JSON.parse(JSON.generate(state), symbolize_names: true)
20
- instance = Runtime::Instance.new(aggregate: aggregate, id: id, state: symbolized)
21
- lifecycle = aggregate.lifecycle
22
- next unless lifecycle
18
+ symbolized = JSON.parse(JSON.generate(state), symbolize_names: true)
19
+ instance = Runtime::Instance.new(aggregate: aggregate, id: id, state: symbolized)
20
+ lifecycle = aggregate.lifecycle
21
+ next unless lifecycle
23
22
 
24
- held = instance[lifecycle.field]
25
- # `Lifecycle#states` answers default+targets only — a state
26
- # legitimately declared just as a `from:` (a terminal
27
- # transition's source, never anyone's target) is real and
28
- # reachable but invisible to it. `ModelCheck.full_states`
29
- # is the full declared set (default, every target, AND
30
- # every from) that `fuzzing/properties.rb`'s own replay
31
- # check already uses for this identical question — see its
32
- # comment on this same hole.
33
- allowed = Bluebook::ModelCheck.full_states(lifecycle)
34
- unless held.nil? || allowed.include?(held.to_s)
35
- violations << "#{aggregate.name}##{id}: #{lifecycle.field} is #{held.inspect}, " \
36
- "a state this era's lifecycle never reaches"
37
- end
38
- rescue Runtime::InvariantViolation, Runtime::TypeMismatch => error
39
- violations << "#{aggregate.name}##{id}: #{error.message}"
23
+ held = instance[lifecycle.field]
24
+ # `Lifecycle#states` answers default+targets only — a state
25
+ # legitimately declared just as a `from:` (a terminal
26
+ # transition's source, never anyone's target) is real and
27
+ # reachable but invisible to it. `ModelCheck.full_states`
28
+ # is the full declared set (default, every target, AND
29
+ # every from) that `fuzzing/properties.rb`'s own replay
30
+ # check already uses for this identical question — see its
31
+ # comment on this same hole.
32
+ allowed = Bluebook::ModelCheck.full_states(lifecycle)
33
+ unless held.nil? || allowed.include?(held.to_s)
34
+ violations << "#{aggregate.name}##{id}: #{lifecycle.field} is #{held.inspect}, " \
35
+ "a state this era's lifecycle never reaches"
40
36
  end
37
+ rescue Runtime::InvariantViolation, Runtime::TypeMismatch => e
38
+ violations << "#{aggregate.name}##{id}: #{e.message}"
41
39
  end
42
40
  end
43
41
  end