hecks 1.4.0 → 1.5.1

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 (275) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +65 -0
  3. data/lib/hecks/adapters/driven/folder.rb +73 -0
  4. data/lib/hecks/adapters/driven/google_authentication.rb +25 -4
  5. data/lib/hecks/adapters/driven/governance_authorization.rb +57 -6
  6. data/lib/hecks/adapters/driven/heki/journal.rb +8 -0
  7. data/lib/hecks/adapters/driven/heki/saga_store.rb +53 -7
  8. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  9. data/lib/hecks/adapters/driven/heki.rb +93 -2
  10. data/lib/hecks/adapters/driven/identity_registry.rb +10 -0
  11. data/lib/hecks/adapters/driven/in_memory_ordering.rb +22 -0
  12. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  13. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  14. data/lib/hecks/adapters/driven/lambda/client.rb +35 -7
  15. data/lib/hecks/adapters/driven/lambda.rb +43 -2
  16. data/lib/hecks/adapters/driven/local_storage.rb +67 -1
  17. data/lib/hecks/adapters/driven/memory.rb +13 -13
  18. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +20 -0
  19. data/lib/hecks/adapters/driven/prism.rb +46 -7
  20. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  21. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  22. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  23. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  24. data/lib/hecks/adapters/driven.rb +2 -0
  25. data/lib/hecks/adapters/driving/github_webhook.rb +14 -1
  26. data/lib/hecks/behaviors/dsl.rb +58 -0
  27. data/lib/hecks/behaviors/expectations.rb +168 -7
  28. data/lib/hecks/behaviors/ir.rb +11 -0
  29. data/lib/hecks/behaviors/rspec.rb +8 -0
  30. data/lib/hecks/behaviors/runner.rb +19 -0
  31. data/lib/hecks/behaviors.rb +8 -0
  32. data/lib/hecks/bluebook/aggregate.rb +30 -0
  33. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +7 -0
  34. data/lib/hecks/bluebook/assembly/build.rb +25 -4
  35. data/lib/hecks/bluebook/assembly/contract.rb +66 -14
  36. data/lib/hecks/bluebook/assembly/contracts.rb +24 -19
  37. data/lib/hecks/bluebook/assembly/marks.rb +143 -14
  38. data/lib/hecks/bluebook/assembly/specializer.rb +19 -0
  39. data/lib/hecks/bluebook/assembly.rb +25 -7
  40. data/lib/hecks/bluebook/attribute.rb +17 -3
  41. data/lib/hecks/bluebook/behaviour/aggregate.rb +32 -0
  42. data/lib/hecks/bluebook/behaviour/attribute.rb +13 -0
  43. data/lib/hecks/bluebook/behaviour/chapter.rb +55 -2
  44. data/lib/hecks/bluebook/behaviour/command.rb +33 -3
  45. data/lib/hecks/bluebook/behaviour/domain_port.rb +20 -0
  46. data/lib/hecks/bluebook/behaviour/entity.rb +12 -0
  47. data/lib/hecks/bluebook/behaviour/hexagon.rb +26 -0
  48. data/lib/hecks/bluebook/behaviour/lifecycle.rb +23 -2
  49. data/lib/hecks/bluebook/behaviour/policy.rb +34 -9
  50. data/lib/hecks/bluebook/behaviour/process_manager.rb +32 -1
  51. data/lib/hecks/bluebook/behaviour/query.rb +5 -0
  52. data/lib/hecks/bluebook/behaviour/read_model.rb +21 -0
  53. data/lib/hecks/bluebook/behaviour/traits.rb +36 -0
  54. data/lib/hecks/bluebook/behaviour/value_object.rb +17 -5
  55. data/lib/hecks/bluebook/chapter.rb +23 -0
  56. data/lib/hecks/bluebook/command.rb +53 -8
  57. data/lib/hecks/bluebook/domain_port.rb +25 -0
  58. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +196 -37
  59. data/lib/hecks/bluebook/dsl/attribute_collector.rb +79 -42
  60. data/lib/hecks/bluebook/dsl/binding_proxy.rb +59 -0
  61. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +385 -53
  62. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +135 -21
  63. data/lib/hecks/bluebook/dsl/command_builder.rb +189 -27
  64. data/lib/hecks/bluebook/dsl/entity_builder.rb +139 -9
  65. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +41 -0
  66. data/lib/hecks/bluebook/dsl/identity_declaration.rb +23 -6
  67. data/lib/hecks/bluebook/dsl/policy_builder.rb +60 -19
  68. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +79 -15
  69. data/lib/hecks/bluebook/dsl/query_builder.rb +33 -4
  70. data/lib/hecks/bluebook/dsl/read_model_builder.rb +102 -27
  71. data/lib/hecks/bluebook/dsl/rule_reference.rb +60 -8
  72. data/lib/hecks/bluebook/dsl/translation_builder.rb +146 -40
  73. data/lib/hecks/bluebook/dsl/value_object_builder.rb +55 -7
  74. data/lib/hecks/bluebook/entity.rb +29 -0
  75. data/lib/hecks/bluebook/expression/ast_json.rb +115 -23
  76. data/lib/hecks/bluebook/expression/ast_reader.rb +29 -0
  77. data/lib/hecks/bluebook/expression/canonical_form.rb +48 -9
  78. data/lib/hecks/bluebook/expression/evaluator.rb +207 -29
  79. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +36 -0
  80. data/lib/hecks/bluebook/expression/resolver.rb +320 -77
  81. data/lib/hecks/bluebook/hexagon.rb +34 -0
  82. data/lib/hecks/bluebook/lifecycle.rb +11 -0
  83. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -0
  84. data/lib/hecks/bluebook/meta_validator/judge.rb +30 -26
  85. data/lib/hecks/bluebook/meta_validator/plan.rb +43 -8
  86. data/lib/hecks/bluebook/meta_validator/port_judge.rb +1 -0
  87. data/lib/hecks/bluebook/meta_validator/readings.rb +157 -7
  88. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -4
  89. data/lib/hecks/bluebook/meta_validator/shapes.rb +141 -0
  90. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +149 -23
  91. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +5 -4
  92. data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -0
  93. data/lib/hecks/bluebook/meta_validator.rb +180 -84
  94. data/lib/hecks/bluebook/model_check.rb +268 -24
  95. data/lib/hecks/bluebook/pattern_subset.rb +23 -1
  96. data/lib/hecks/bluebook/process_manager.rb +13 -0
  97. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  98. data/lib/hecks/bluebook/project_loader.rb +40 -0
  99. data/lib/hecks/bluebook/project_register.rb +44 -0
  100. data/lib/hecks/bluebook/query.rb +27 -0
  101. data/lib/hecks/bluebook/read_model.rb +21 -1
  102. data/lib/hecks/bluebook/reference.rb +21 -8
  103. data/lib/hecks/bluebook/smoke_test.rb +29 -6
  104. data/lib/hecks/bluebook/synthesizer.rb +34 -0
  105. data/lib/hecks/bluebook/translation.rb +30 -1
  106. data/lib/hecks/bluebook/value_object.rb +23 -5
  107. data/lib/hecks/bluebook.rb +3 -4
  108. data/lib/hecks/codemod.rb +107 -20
  109. data/lib/hecks/construct.rb +15 -1
  110. data/lib/hecks/corpus.rb +146 -25
  111. data/lib/hecks/deploy/bluebook/deploy.bluebook +105 -0
  112. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  113. data/lib/hecks/deploy/oidc.json +5 -0
  114. data/lib/hecks/doc/reference.rb +185 -16
  115. data/lib/hecks/embryonaut_bluebook.rb +32 -9
  116. data/lib/hecks/facade/handle.rb +76 -3
  117. data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
  118. data/lib/hecks/forms/field_shape.rb +3 -0
  119. data/lib/hecks/forms/page.rb +14 -0
  120. data/lib/hecks/forms/port_argument.rb +12 -0
  121. data/lib/hecks/forms/query_form_renderer.rb +63 -0
  122. data/lib/hecks/forms/record_renderer.rb +58 -0
  123. data/lib/hecks/forms/record_table.rb +27 -0
  124. data/lib/hecks/forms/reference_options.rb +24 -0
  125. data/lib/hecks/forms/value_object_shape.rb +10 -0
  126. data/lib/hecks/fqn.rb +58 -0
  127. data/lib/hecks/framework/bluebook/compliance.bluebook +221 -0
  128. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  129. data/lib/hecks/framework/oidc.json +15 -0
  130. data/lib/hecks/framework.rb +43 -20
  131. data/lib/hecks/freezer.rb +17 -1
  132. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
  133. data/lib/hecks/fuzzing/combination_miner.rb +59 -0
  134. data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
  135. data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
  136. data/lib/hecks/fuzzing/differential.rb +34 -0
  137. data/lib/hecks/fuzzing/domain_generator.rb +188 -11
  138. data/lib/hecks/fuzzing/era_boundary.rb +45 -15
  139. data/lib/hecks/fuzzing/form_census.rb +86 -0
  140. data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
  141. data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
  142. data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
  143. data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
  144. data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
  145. data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
  146. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
  147. data/lib/hecks/fuzzing/properties/guards.rb +44 -0
  148. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
  149. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
  150. data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
  151. data/lib/hecks/fuzzing/properties/querying.rb +68 -14
  152. data/lib/hecks/fuzzing/properties.rb +24 -15
  153. data/lib/hecks/fuzzing/qa_settings.rb +12 -0
  154. data/lib/hecks/fuzzing/replay.rb +137 -29
  155. data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
  156. data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
  157. data/lib/hecks/fuzzing/self_consistency.rb +189 -40
  158. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
  159. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
  160. data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
  161. data/lib/hecks/fuzzing/shrinker.rb +114 -2
  162. data/lib/hecks/fuzzing/structural_skips.rb +18 -4
  163. data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
  164. data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
  165. data/lib/hecks/fuzzing/value_generator.rb +98 -10
  166. data/lib/hecks/grammar/evolve.rb +178 -2
  167. data/lib/hecks/grammar.rb +46 -0
  168. data/lib/hecks/ir.rb +38 -7
  169. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  170. data/lib/hecks/literal.rb +32 -0
  171. data/lib/hecks/naming.rb +88 -7
  172. data/lib/hecks/ports/access_control.rb +5 -10
  173. data/lib/hecks/ports/authorization.rb +3 -6
  174. data/lib/hecks/ports/identity_assignment.rb +1 -2
  175. data/lib/hecks/ports/identity_resolution.rb +1 -2
  176. data/lib/hecks/ports/key_vault.port +6 -0
  177. data/lib/hecks/ports/key_vault.rb +85 -0
  178. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
  179. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
  180. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
  181. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
  182. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
  183. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  184. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
  185. data/lib/hecks/ports/query/in_memory.rb +39 -6
  186. data/lib/hecks/ports/query/ordering.rb +15 -0
  187. data/lib/hecks/ports.rb +1 -0
  188. data/lib/hecks/projections/bootstrap_table.rb +43 -8
  189. data/lib/hecks/projections/deploy/fargate.rb +666 -0
  190. data/lib/hecks/projections/deploy/lambda.rb +2423 -0
  191. data/lib/hecks/projections/deploy/shared.rb +624 -0
  192. data/lib/hecks/projections/diagrams.rb +243 -7
  193. data/lib/hecks/projections/glossary/html.rb +88 -0
  194. data/lib/hecks/projections/glossary/markdown.rb +47 -0
  195. data/lib/hecks/projections/glossary/mermaid.rb +48 -0
  196. data/lib/hecks/projections/glossary/sentences.rb +105 -0
  197. data/lib/hecks/projections/glossary.rb +161 -19
  198. data/lib/hecks/projections/model/deviations.rb +44 -0
  199. data/lib/hecks/projections/model.rb +51 -1
  200. data/lib/hecks/projections/oidc.rb +40 -3
  201. data/lib/hecks/projections/parser_table.rb +49 -8
  202. data/lib/hecks/projections/reference.rb +12 -0
  203. data/lib/hecks/projections/rust_vocabulary.rb +219 -16
  204. data/lib/hecks/projections/shape.rb +8 -0
  205. data/lib/hecks/projections/statements.rb +63 -16
  206. data/lib/hecks/projections/vocabulary.rb +17 -0
  207. data/lib/hecks/projections.rb +3 -0
  208. data/lib/hecks/projector/cli_projector.rb +218 -10
  209. data/lib/hecks/projector/docs_projector.rb +145 -19
  210. data/lib/hecks/projector/exporter.rb +65 -11
  211. data/lib/hecks/projector/ir_projector.rb +6 -0
  212. data/lib/hecks/projector/narrate_projector.rb +136 -15
  213. data/lib/hecks/projector/target.rb +47 -10
  214. data/lib/hecks/projector.rb +101 -19
  215. data/lib/hecks/query_ir.rb +47 -0
  216. data/lib/hecks/query_specification/common/null_policy.rb +5 -3
  217. data/lib/hecks/rendering.rb +6 -0
  218. data/lib/hecks/router/namespace_installer.rb +13 -0
  219. data/lib/hecks/router.rb +55 -0
  220. data/lib/hecks/runtime/aggregate_lock.rb +9 -0
  221. data/lib/hecks/runtime/boot_gates.rb +18 -0
  222. data/lib/hecks/runtime/caller.rb +32 -0
  223. data/lib/hecks/runtime/capability_graph.rb +11 -0
  224. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
  225. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
  226. data/lib/hecks/runtime/command_interpreter.rb +42 -17
  227. data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
  228. data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
  229. data/lib/hecks/runtime/command_rules/references.rb +118 -28
  230. data/lib/hecks/runtime/dependency_planning.rb +45 -0
  231. data/lib/hecks/runtime/dispatcher.rb +28 -50
  232. data/lib/hecks/runtime/entity_element.rb +161 -8
  233. data/lib/hecks/runtime/entity_interpreter.rb +44 -9
  234. data/lib/hecks/runtime/errors.rb +18 -4
  235. data/lib/hecks/runtime/event.rb +10 -5
  236. data/lib/hecks/runtime/identity.rb +71 -3
  237. data/lib/hecks/runtime/instance.rb +67 -7
  238. data/lib/hecks/runtime/interpreting.rb +13 -5
  239. data/lib/hecks/runtime/invocation.rb +118 -36
  240. data/lib/hecks/runtime/loader.rb +94 -8
  241. data/lib/hecks/runtime/outbox.rb +145 -7
  242. data/lib/hecks/runtime/policy_interpreter.rb +22 -9
  243. data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
  244. data/lib/hecks/runtime/query_interpreter.rb +40 -12
  245. data/lib/hecks/runtime/reaction_invocation.rb +53 -8
  246. data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
  247. data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
  248. data/lib/hecks/runtime/reference_hop.rb +42 -0
  249. data/lib/hecks/runtime/refusal_wording.rb +50 -0
  250. data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
  251. data/lib/hecks/runtime/registry/verification.rb +119 -4
  252. data/lib/hecks/runtime/registry.rb +157 -4
  253. data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
  254. data/lib/hecks/runtime/routing.rb +27 -2
  255. data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
  256. data/lib/hecks/runtime/saga_interpreter.rb +27 -13
  257. data/lib/hecks/runtime/tenant_check.rb +26 -6
  258. data/lib/hecks/runtime/tenant_scope.rb +18 -0
  259. data/lib/hecks/runtime/value/coercion.rb +255 -33
  260. data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
  261. data/lib/hecks/runtime/value.rb +50 -0
  262. data/lib/hecks/runtime.rb +32 -0
  263. data/lib/hecks/storehouse.rb +305 -9
  264. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  265. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  266. data/lib/hecks/version.rb +1 -1
  267. data/lib/hecks.rb +79 -1
  268. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
  269. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
  270. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
  271. metadata +14 -5
  272. data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
  273. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
  274. data/lib/hecks/deprecation.rb +0 -95
  275. data/lib/hecks/framework/bluebook/compliance.bluebook +0 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: badbf552a5137cc39f791c4a398fe7d33423ff6d15c82cb018d1ef7c464af754
4
- data.tar.gz: 5bf8d933f52178d2870d19dd0e33b03cacd060b848b54340c4940edbe94e008e
3
+ metadata.gz: 29c54ce86e36189a63578e970929e42c213d5812282c5cf5ca6e56820eb02938
4
+ data.tar.gz: 1d564ee387fb38c619fbdfef209bcc757570a492e70dac16f1d5fb80889c37e8
5
5
  SHA512:
6
- metadata.gz: 9327ee7366dacb9dcc90fc155999e5ff055efefc8e840ff4ddb08a7042adfcedbe5396c7547713562bebe1db618017b6851421f48d0ac300ab4339cf674a4cca
7
- data.tar.gz: 97e1aa8115377269a23b29609e88370f86cfaec8dee9f79889232755dea7a55921d7e3db2fa6c3af26ca4f2afaca56a034fd7a6b5b8413ad00e45aef9c24c0d9
6
+ metadata.gz: 24794f4aaa4f5d2268f27e063b79a23b61c7dffe7b4a87b4b58f83db73188d57716fd734d695082bdf11d78d3101fc41640a45f76510fe7cc45fd9c3cd9347a8
7
+ data.tar.gz: 9ce51558bbe7c55e9dc451140a21d7c83b7073b8a692304579f15c76db3c56371cb90c7cac07b6c02e6eb64ddd73c27572e786ab941659d2a76e649de5d2a699
@@ -40,10 +40,22 @@ module Hecks
40
40
 
41
41
  module_function
42
42
 
43
+ # Asks the model for the next best interview question, as a raw parsed-JSON reply.
44
+ #
43
45
  # **The next best question**. `state` is whatever
44
46
  # `Interview::Session#declaration`/`#gaps` produced — passed
45
47
  # through as JSON, not reformatted, so this adapter never
46
48
  # re-derives what the session already knows.
49
+ #
50
+ # @param state [Hash] the interview's whole current picture (declaration plus gaps),
51
+ # JSON-able
52
+ # @param asked [Array<Object>] JSON-able record of the questions already asked
53
+ # @return [Object] the parsed JSON reply, expected to be a Hash holding a `"questions"`
54
+ # array; shape validated downstream by `Ports::Agent::Answers.questions`
55
+ # @raise [Ports::Agent::Unavailable] if the `claude` binary is missing, the subprocess
56
+ # fails, or the call times out
57
+ # @raise [Ports::Agent::ValidationError] if the CLI's own envelope has no `"result"`, or
58
+ # the result text is not valid JSON
47
59
  def ask(state:, asked:)
48
60
  call(
49
61
  system: SYSTEM_PREFIX + "Given the domain model so far, ask the single best next " \
@@ -53,7 +65,18 @@ module Hecks
53
65
  )
54
66
  end
55
67
 
68
+ # Turns a human's sentence into proposed declarations, as a raw parsed-JSON reply.
69
+ #
56
70
  # Prose -> proposed declarations.
71
+ #
72
+ # @param prose [String] a human's plain-English sentence
73
+ # @param state [Hash] the interview's whole current picture, JSON-able
74
+ # @return [Object] the parsed JSON reply, expected to be a Hash holding a `"proposals"`
75
+ # array; shape validated downstream by `Ports::Agent::Answers.proposals`
76
+ # @raise [Ports::Agent::Unavailable] if the `claude` binary is missing, the subprocess
77
+ # fails, or the call times out
78
+ # @raise [Ports::Agent::ValidationError] if the CLI's own envelope has no `"result"`, or
79
+ # the result text is not valid JSON
57
80
  def interpret(prose:, state:)
58
81
  call(
59
82
  system: SYSTEM_PREFIX + "Given the domain model so far and a sentence the human just said, " \
@@ -66,10 +89,22 @@ module Hecks
66
89
  )
67
90
  end
68
91
 
92
+ # Asks the model to judge a declared model on taste, as a raw parsed-JSON reply.
93
+ #
69
94
  # What is wrong with this as a model — closed to the same kind
70
95
  # vocabulary `Ports::Agent::CRITIQUE_KINDS` declares, spelled out
71
96
  # here too since the system prompt is the only place the model
72
97
  # itself ever sees that list.
98
+ #
99
+ # @param declared [Hash] the chapter as declared so far, JSON-able
100
+ # @param refusals [Array<Object>] JSON-able refusals the language itself already raised
101
+ # @param findings [Array<Object>] JSON-able mechanical findings already found
102
+ # @return [Object] the parsed JSON reply, expected to be a Hash holding a `"findings"`
103
+ # array; shape validated downstream by `Ports::Agent::Answers.findings`
104
+ # @raise [Ports::Agent::Unavailable] if the `claude` binary is missing, the subprocess
105
+ # fails, or the call times out
106
+ # @raise [Ports::Agent::ValidationError] if the CLI's own envelope has no `"result"`, or
107
+ # the result text is not valid JSON
73
108
  def critique(declared:, refusals:, findings:)
74
109
  kinds = Ports::Agent::CRITIQUE_KINDS.join(", ")
75
110
  call(
@@ -82,9 +117,22 @@ module Hecks
82
117
  )
83
118
  end
84
119
 
120
+ # Suggests a name for a construct, as a raw parsed-JSON reply.
121
+ #
85
122
  # **Vocabulary help**. Named `suggest_name`, not `name` — see
86
123
  # `Ports::Agent#suggest_name`'s own comment for why `name` is
87
124
  # never a safe module-function name here.
125
+ #
126
+ # @param meaning [String] what the new name needs to mean
127
+ # @param kind [String] the kind of construct being named, such as `"event"`
128
+ # @param near [Array<String>] names already in use nearby, which a suggestion must not
129
+ # collide with
130
+ # @return [Object] the parsed JSON reply, expected to be a Hash holding a `"names"`
131
+ # array; shape validated downstream by `Ports::Agent::Answers.suggestions`
132
+ # @raise [Ports::Agent::Unavailable] if the `claude` binary is missing, the subprocess
133
+ # fails, or the call times out
134
+ # @raise [Ports::Agent::ValidationError] if the CLI's own envelope has no `"result"`, or
135
+ # the result text is not valid JSON
88
136
  def suggest_name(meaning:, kind:, near:)
89
137
  call(
90
138
  system: SYSTEM_PREFIX + "Suggest a name for a #{kind} meaning \"#{meaning}\", distinct from " \
@@ -96,6 +144,16 @@ module Hecks
96
144
 
97
145
  # ── transport ───────────────────────────────────────────────────
98
146
 
147
+ # Runs `claude` as a subprocess with `payload` on stdin and returns its parsed JSON reply.
148
+ #
149
+ # @param system [String] the system-prompt text appended via `--append-system-prompt`
150
+ # @param payload [Hash] the JSON-able payload written to the subprocess's stdin
151
+ # @return [Object] the parsed JSON value nested under the CLI envelope's `"result"` key
152
+ # (see `unwrap`)
153
+ # @raise [Ports::Agent::Unavailable] if the `claude` binary is missing, the subprocess
154
+ # exits non-zero, or the call does not finish within `TIMEOUT_SECONDS`
155
+ # @raise [Ports::Agent::ValidationError] if the envelope has no `"result"`, or the
156
+ # result text is not valid JSON (see `unwrap`)
99
157
  def call(system:, payload:)
100
158
  stdout, status = Timeout.timeout(TIMEOUT_SECONDS) do
101
159
  Open3.capture2(
@@ -113,6 +171,13 @@ module Hecks
113
171
  raise Ports::Agent::Unavailable, "claude is not on PATH: #{e.message}"
114
172
  end
115
173
 
174
+ # Unwraps the CLI's own JSON envelope down to the model's parsed reply.
175
+ #
176
+ # @param stdout [String] the raw stdout of the `claude` CLI invocation, the outer
177
+ # `{"result": "..."}` envelope
178
+ # @return [Object] the JSON value parsed out of the envelope's `"result"` string
179
+ # @raise [Ports::Agent::ValidationError] if the envelope has no `"result"` key, or
180
+ # `result` is not valid JSON
116
181
  def unwrap(stdout)
117
182
  envelope = JSON.parse(stdout)
118
183
  result = envelope["result"]
@@ -14,16 +14,26 @@ module Hecks
14
14
  PORTS = "ports".freeze
15
15
  ADAPTERS = "adapters".freeze
16
16
 
17
+ # @param settings [Hash] accepted but not read by this class
18
+ # @param root [String, nil] accepted but not read by this class
17
19
  def initialize(settings: {}, root: nil)
18
20
  @settings = settings
19
21
  @root = root
20
22
  end
21
23
 
24
+ # Loads the framework's own bundled ports and adapters from `lib/hecks/{ports,adapters}`.
25
+ #
26
+ # @return [void]
22
27
  def load_library
23
28
  load_each(library(PORTS), %w[*.port])
24
29
  load_each(library(ADAPTERS), %w[*/*.adapter */*/*.adapter])
25
30
  end
26
31
 
32
+ # Loads a shared root's own ports and adapters, if one was found.
33
+ #
34
+ # @param root [String, nil] the shared root directory, as `shared_root` resolves it;
35
+ # nil is a no-op
36
+ # @return [void]
27
37
  def load_project(root)
28
38
  return unless root
29
39
 
@@ -39,6 +49,7 @@ module Hecks
39
49
  # already places every `*.bluebook` ahead of hecksagons and worlds,
40
50
  # so the window ends at the last chapter pattern rather than at a
41
51
  # hand-written list this would otherwise have to keep in step.
52
+ #
42
53
  # `environment:` — one more pair of files, loaded last, not a glob.
43
54
  # Recovered, not new — see Runtime::Loader.boot's own comment for
44
55
  # the provenance. A caller passing `Hecks.boot(path, environment:
@@ -58,6 +69,12 @@ module Hecks
58
69
  # concatenate/override rather than replace), so an overlay can
59
70
  # rebind or add settings for anything the base file declared
60
71
  # without needing to know what else the base file said.
72
+ #
73
+ # @param directory [String] the domain's bluebook directory to load, as
74
+ # `bluebook_directory` resolves it
75
+ # @param environment [String, nil] the environment overlay to load after the domain's
76
+ # own files (e.g. `"production"`); nil loads no overlay
77
+ # @return [void]
61
78
  def load_domain(directory, environment: nil)
62
79
  boundary = DOMAIN_ORDER.rindex { |pattern| pattern.end_with?(".bluebook") }
63
80
  if boundary
@@ -79,6 +96,11 @@ module Hecks
79
96
  # each file, so a folder may hold more than one chapter without a catalog.
80
97
  # Sorting makes source order deterministic while the deferred window keeps
81
98
  # cross-file references from being judged against a partial chapter.
99
+ #
100
+ # @param directory [String] the directory to glob for bluebook chapter files
101
+ # @param patterns [Array<String>] glob patterns, relative to `directory`, selecting the
102
+ # chapter files to load
103
+ # @return [void]
82
104
  def load_bluebooks(directory, patterns = ["*.bluebook"])
83
105
  Bluebook::MetaValidator.defer { load_each(directory, patterns) }
84
106
  Bluebook::MetaValidator.judge_deferred!(Hecks.current_registry)
@@ -101,6 +123,10 @@ module Hecks
101
123
  # "x.hecksagon", "x.bluebook"` in. Bluebook chapters are judged as one
102
124
  # deferred group exactly like `load_domain` does, for the identical
103
125
  # forward-reference reason (MetaValidator.defer's own header).
126
+ # @param files [Array<String>] file paths to load, relative to `bluebook_directory`
127
+ # @param environment [String, nil] the environment name whose overlay, if present,
128
+ # loads after the selected files
129
+ # @return [void]
104
130
  def load_selected(files, environment: nil)
105
131
  bluebooks, rest = files.partition { |f| f.end_with?(".bluebook") }
106
132
 
@@ -120,6 +146,14 @@ module Hecks
120
146
  load_each(directory, [File.join("environments", "#{environment}.world")])
121
147
  end
122
148
 
149
+ # Loads every file under `directory` matching any of `patterns`, in pattern order.
150
+ #
151
+ # A no-op if `directory` does not exist.
152
+ #
153
+ # @param directory [String] the directory to search
154
+ # @param patterns [Array<String>] glob patterns, relative to `directory`, of files to
155
+ # load with `Kernel.load`
156
+ # @return [void]
123
157
  def load_each(directory, patterns)
124
158
  return unless File.directory?(directory)
125
159
 
@@ -128,6 +162,13 @@ module Hecks
128
162
  end
129
163
  end
130
164
 
165
+ # Resolves a domain path to the directory that actually holds its bluebook files.
166
+ #
167
+ # @param path [String] a domain directory, holding either a `bluebook/` subdirectory or
168
+ # its chapter files directly
169
+ # @return [String] the `bluebook/` subdirectory's absolute path, if one exists; `path`'s
170
+ # own absolute path otherwise
171
+ # @raise [Errno::ENOENT] if neither directory exists
131
172
  def bluebook_directory(path)
132
173
  expanded = File.expand_path(path)
133
174
  nested = File.join(expanded, "bluebook")
@@ -138,6 +179,8 @@ module Hecks
138
179
  raise Errno::ENOENT, "no such domain directory: #{path}"
139
180
  end
140
181
 
182
+ # Finds the nearest domain directory at or above `from`, or nil if there is not one.
183
+ #
141
184
  # **The domain you are standing in**. Walks up from `from` — the way git
142
185
  # finds `.git` — and answers the nearest directory a boot would accept,
143
186
  # or nil if there is not one above you.
@@ -159,6 +202,11 @@ module Hecks
159
202
  # so the registry root comes out the same — but `examples/banking` is the
160
203
  # directory a person names, and the one a `.world`'s `dir "data"` reads
161
204
  # as relative to.
205
+ #
206
+ # @param from [String] the directory to walk up from; defaults to the process's current
207
+ # working directory
208
+ # @return [String, nil] the resolved domain directory's absolute path, or nil if none is
209
+ # found above `from`
162
210
  def domain_root(from = Dir.pwd)
163
211
  found = nearest_domain(File.expand_path(from))
164
212
  return nil unless found
@@ -167,6 +215,12 @@ module Hecks
167
215
  File.basename(found) == "bluebook" && domain?(parent) ? parent : found
168
216
  end
169
217
 
218
+ # Walks up from `current` until it finds a domain directory, or reaches the filesystem
219
+ # root.
220
+ #
221
+ # @param current [String] the absolute directory path to start searching from
222
+ # @return [String, nil] the nearest directory (`current` or an ancestor) that `domain?`
223
+ # accepts, or nil if none is found before the filesystem root
170
224
  def nearest_domain(current)
171
225
  loop do
172
226
  return current if domain?(current)
@@ -178,11 +232,25 @@ module Hecks
178
232
  end
179
233
  end
180
234
 
235
+ # Reports whether `directory` is a domain root.
236
+ #
237
+ # @param directory [String] the directory to check
238
+ # @return [Boolean] true if `directory`, or its `bluebook/` subdirectory, holds a
239
+ # `.hecksagon` file
181
240
  def domain?(directory)
182
241
  !Dir[File.join(directory, "*.hecksagon")].empty? ||
183
242
  !Dir[File.join(directory, "bluebook", "*.hecksagon")].empty?
184
243
  end
185
244
 
245
+ # Resolves the shared root a domain's own ports/adapters overlay from.
246
+ #
247
+ # @param given [String, nil] an explicit shared-root override; returned expanded as-is
248
+ # when present
249
+ # @param directory [String] the domain directory to search upward from when `given` is
250
+ # nil
251
+ # @return [String, nil] `given`'s expanded path, or the nearest ancestor of `directory`
252
+ # holding a `ports` or `adapters` folder; nil if none is found before the filesystem
253
+ # root
186
254
  def shared_root(given, directory)
187
255
  return File.expand_path(given) if given
188
256
 
@@ -198,6 +266,11 @@ module Hecks
198
266
  end
199
267
  end
200
268
 
269
+ # Resolves the framework's own bundled `folder` directory under `lib/hecks`.
270
+ #
271
+ # @param folder [String] `"ports"` or `"adapters"`, the framework's own bundled
272
+ # directory name
273
+ # @return [String] the absolute path to `lib/hecks/<folder>`
201
274
  def library(folder)
202
275
  File.expand_path("../../#{folder}", __dir__)
203
276
  end
@@ -5,11 +5,11 @@ require_relative "../../ports/authentication"
5
5
  module Hecks
6
6
  module Adapters
7
7
  # **Google's own OIDC handshake** — the `authentication` port's one real
8
- # implementation today, moved here from being hand-rolled per-app
9
- # (an embryonaut_console `google_auth.rb` used to do exactly this;
10
- # any hecks-based app gets Google sign-in for free now, the
8
+ # implementation today. Consolidates what was hand-rolled per-app
9
+ # (an embryonaut_console `google_auth.rb` did this on its own) into
10
+ # one adapter, so any hecks-based app gets Google sign-in for free, the
11
11
  # same "one adapter, reusable everywhere" value every other adapter
12
- # in this directory already has).
12
+ # in this directory already has.
13
13
  #
14
14
  # `oauth2` does only the authorization-code exchange (no Rack
15
15
  # middleware, no Omniauth strategy indirection) ; `google-id-token`
@@ -34,6 +34,10 @@ module Hecks
34
34
 
35
35
  module_function
36
36
 
37
+ # Builds the OAuth2 client for Google's token endpoints.
38
+ #
39
+ # @return [OAuth2::Client] a client configured for Google's OAuth2/OIDC endpoints
40
+ # @raise [KeyError] if `GOOGLE_CLIENT_ID` or `GOOGLE_CLIENT_SECRET` is unset
37
41
  def client
38
42
  require "oauth2"
39
43
  OAuth2::Client.new(
@@ -47,6 +51,11 @@ module Hecks
47
51
  # The URL to send a browser to, carrying a fresh CSRF `state` the
48
52
  # caller is responsible for stashing (a session, typically) and
49
53
  # checking again in `verify`.
54
+ #
55
+ # @return [Array(String, String)] the URL to send the browser to, and the fresh CSRF
56
+ # `state` embedded in it
57
+ # @raise [KeyError] if `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` or
58
+ # `GOOGLE_REDIRECT_URI` is unset
50
59
  def authorization_url
51
60
  state = SecureRandom.hex(24)
52
61
  url = client.auth_code.authorize_url(
@@ -67,6 +76,18 @@ module Hecks
67
76
  # the raw token, never anything a caller would need to re-verify
68
77
  # itself. `email_verified` rides along because a caller granting
69
78
  # access off this email needs to know Google actually checked it.
79
+ #
80
+ # @param code [String] the authorization code the provider sent back
81
+ # @param state [String, nil] the state parameter the provider returned; nil is refused
82
+ # @param expected_state [String, nil] the state `authorization_url` handed out before
83
+ # the redirect; nil is refused
84
+ # @return [Hash{Symbol => Object}] `issuer:` and `subject:` (String), `email:` (String,
85
+ # or nil if the token carries none), `email_verified:` (Boolean)
86
+ # @raise [Ports::Authentication::ValidationError] if either state is nil or the two
87
+ # differ, the code exchange fails, the response has no ID token, or the ID token does
88
+ # not verify
89
+ # @raise [KeyError] if `GOOGLE_CLIENT_ID` or `GOOGLE_REDIRECT_URI` is unset, or a
90
+ # verified token lacks an `iss` or `sub` claim
70
91
  def verify(code:, state:, expected_state:)
71
92
  # **Both gems, before anything else** — not staggered further down
72
93
  # this method: the rescue clause below names GoogleIDToken
@@ -12,12 +12,12 @@ module Hecks
12
12
  # dependency on when Governance's bluebook loads relative to this
13
13
  # adapter, only that it has by the time `holds_role?` is called.
14
14
  #
15
- # An active assignment, not merely a historical one : `RoleAssignment`
16
- # answers with every assignment an actor has ever held
17
- # (`AssignmentsForActor`'s own description — "currently or
18
- # historically") and leaves `ends_at` for the caller to read, the
19
- # same deferral `Governance::RoleTransition.Allowed` makes for the
20
- # same reason. This is that caller.
15
+ # An active assignment, not merely a past one — `RoleAssignment`
16
+ # answers with every assignment an actor has ever held, current or
17
+ # past (`AssignmentsForActor`'s own description covers both), and
18
+ # leaves `ends_at` for the caller to read, the same deferral
19
+ # `Governance::RoleTransition.Allowed` makes for the same reason.
20
+ # This is that caller.
21
21
  module GovernanceAuthorization
22
22
  module_function
23
23
 
@@ -29,6 +29,18 @@ module Hecks
29
29
  # resolved from `Ports::Clock.now`, called by the caller at the
30
30
  # door, never by this adapter — see `Ports::Clock`'s own header
31
31
  # for why the dispatch path must not consult the clock itself.
32
+ #
33
+ # @param registry [Runtime::Registry] the booted registry, to resolve the authorization
34
+ # provider's verb
35
+ # @param actor_id [String] the actor whose grants are checked, compared as a String
36
+ # @param role [String, Symbol] the role name to look for, compared as a String
37
+ # @param as_of [Integer, nil] Unix epoch seconds; a grant whose `starts_at` is later, or
38
+ # does not parse as a time, does not count. nil skips the `starts_at` check
39
+ # @param scope [String, nil] the scope the caller acts in; nil skips the scope check
40
+ # @return [Boolean] true if at least one live grant of `role` to `actor_id` passes the
41
+ # `as_of` and `scope` checks
42
+ # @raise [Runtime::WiringError] if the loaded chapters providing `"authorization"` are
43
+ # not exactly one (see `provided_verb`)
32
44
  def holds_role?(registry, actor_id:, role:, as_of: nil, scope: nil)
33
45
  rows = Runtime::Dispatcher.new(registry).query(
34
46
  provided_verb(registry, :assignments),
@@ -48,6 +60,11 @@ module Hecks
48
60
  # in gets the pre-scope behavior: any live assignment for the role
49
61
  # authorizes, everywhere. A caller that does state one only
50
62
  # authorizes against an assignment granted for that scope.
63
+ #
64
+ # @param row [Hash{Symbol => Object}] one `RoleAssignment` row as
65
+ # `Runtime::Dispatcher#query` returns it; `:scope` holds a `{value: String}` Hash
66
+ # @param scope [String, nil] the scope to check against; nil accepts any scope
67
+ # @return [Boolean] true if `scope` is nil, or the row's own scope matches it
51
68
  def in_scope?(row, scope)
52
69
  scope.nil? || row[:scope][:value] == scope.to_s
53
70
  end
@@ -59,6 +76,12 @@ module Hecks
59
76
  # Fails closed : a `starts_at` that does not parse is treated as
60
77
  # not-yet-started rather than silently ignored, the same direction
61
78
  # every other check in this method already fails.
79
+ #
80
+ # @param row [Hash{Symbol => Object}] one `RoleAssignment` row as
81
+ # `Runtime::Dispatcher#query` returns it; `:starts_at` holds a `{value: String}` Hash
82
+ # @param as_of [Integer, nil] Unix epoch seconds to compare against; nil accepts any row
83
+ # @return [Boolean] true if `as_of` is nil, or the row's `starts_at` parses and is at or
84
+ # before `as_of`; false if `starts_at` does not parse as a time
62
85
  def started?(row, as_of)
63
86
  return true if as_of.nil?
64
87
 
@@ -67,11 +90,23 @@ module Hecks
67
90
  false
68
91
  end
69
92
 
93
+ # Answers whether one role may act as another.
94
+ #
70
95
  # **The other half** — may role X act as role Y. `RoleTransition.Allowed`
71
96
  # is identified by the exact pair, so at most one row ever comes
72
97
  # back ; still read as `.any?` rather than trusting that structurally,
73
98
  # the same defensiveness `holds_role?` already has to have anyway
74
99
  # since `AssignmentsForActor` can return several.
100
+ #
101
+ # @param registry [Runtime::Registry] the booted registry, to resolve the authorization
102
+ # provider's verb
103
+ # @param from_role [String, Symbol] the role the caller holds, compared as a String
104
+ # @param to_role [String, Symbol] the role the caller wants to act as, compared as a
105
+ # String
106
+ # @return [Boolean] true if a live (not ended) allowance lets `from_role` act as
107
+ # `to_role`
108
+ # @raise [Runtime::WiringError] if the loaded chapters providing `"authorization"` are
109
+ # not exactly one (see `provided_verb`)
75
110
  def authorized_as?(registry, from_role:, to_role:)
76
111
  rows = Runtime::Dispatcher.new(registry).query(
77
112
  provided_verb(registry, :transitions),
@@ -87,6 +122,14 @@ module Hecks
87
122
  # against a caller-supplied guess. `nil` for no live assignment at
88
123
  # all — the caller's own fallback (an aggregate's own role field,
89
124
  # a default) is domain-specific and does not belong here.
125
+ #
126
+ # @param registry [Runtime::Registry] the booted registry, to resolve the authorization
127
+ # provider's verb
128
+ # @param actor_id [String] the actor to look up, compared as a String
129
+ # @return [String, nil] the role name of the actor's first live (not ended) grant, or
130
+ # nil if it has none
131
+ # @raise [Runtime::WiringError] if the loaded chapters providing `"authorization"` are
132
+ # not exactly one (see `provided_verb`)
90
133
  def live_role_for(registry, actor_id:)
91
134
  rows = Runtime::Dispatcher.new(registry).query(
92
135
  provided_verb(registry, :assignments),
@@ -102,6 +145,14 @@ module Hecks
102
145
  # loaded chapter declares it (Governance's, in every boot today).
103
146
  # Exactly one provider, the same "the runtime will not choose for
104
147
  # you" rule `Ports::Authorization.adapter` applies to adapters.
148
+ #
149
+ # @param registry [Runtime::Registry] the booted registry to search for the
150
+ # `"authorization"` provider
151
+ # @param key [Symbol] which declared verb to read — `:assignments`, `:grant`, or
152
+ # `:transitions`
153
+ # @return [String] the fully-qualified verb the loaded chapter provides for `key`
154
+ # @raise [Runtime::WiringError] if the loaded chapters providing `"authorization"` are
155
+ # not exactly one
105
156
  def provided_verb(registry, key)
106
157
  providers = registry.authorization_providers
107
158
  unless providers.size == 1
@@ -6,6 +6,11 @@ module Hecks
6
6
  # The append-only journal beside the snapshot: one JSON line per
7
7
  # entry, fsynced on append, replayed over the snapshot on read.
8
8
  module Journal
9
+ # Reads the whole journal back in append order, for `AppendOnly#recover!` to replay.
10
+ #
11
+ # @return [Array<Ports::Persistence::Entry>] every journalled entry, state decoded
12
+ # through the state codec; `[]` when the journal file does not exist or is empty
13
+ # @raise [Malformed] if a journal line is not valid JSON
9
14
  def entries
10
15
  return [] unless File.exist?(@journal_path)
11
16
 
@@ -46,6 +51,9 @@ module Hecks
46
51
  # once. A crash before `write` completes leaves the journal
47
52
  # fully intact and the prior snapshot untouched, exactly today's
48
53
  # existing crash-recovery guarantee.
54
+ #
55
+ # @return [void]
56
+ # @raise [Malformed] if the snapshot or journal file is corrupt
49
57
  def compact!
50
58
  with_lock do
51
59
  current = replay_journal(read_snapshot)
@@ -4,13 +4,19 @@ require_relative "journal"
4
4
  module Hecks
5
5
  module Adapters
6
6
  class Heki
7
- # The optional saga-persistence capability (§2), Heki's own shape —
8
- # a sibling snapshot+journal file pair, built the exact same way an
9
- # aggregate's own persistence already is: `Snapshot`/`Journal`
10
- # (heki/snapshot.rb, heki/journal.rb) operate generically on
11
- # `@path`/`@journal_path`/`@entry_mirrors` and never touch
12
- # `@aggregate`, so this reuses them unchanged rather than
13
- # re-deriving the same binary framing and crash-recovery replay.
7
+ # The optional saga-persistence capability (§2)'s Heki-specific store — a sibling
8
+ # snapshot+journal file pair, keyed by (domain, process manager, correlation).
9
+ #
10
+ # ## Why a sibling file pair, not a table
11
+ #
12
+ # Built the exact same way an aggregate's own persistence already
13
+ # is: `Snapshot`/`Journal` (heki/snapshot.rb, heki/journal.rb)
14
+ # operate generically on `@path`/`@journal_path`/`@entry_mirrors`
15
+ # and never touch `@aggregate`, so this reuses them unchanged
16
+ # rather than re-deriving the same binary framing and
17
+ # crash-recovery replay.
18
+ #
19
+ # ## Where the file lives
14
20
  #
15
21
  # Reserved file name (`hecks_saga_instances.heki`, matching the
16
22
  # `hecks_`-prefix convention every other new saga table in this
@@ -24,11 +30,15 @@ module Hecks
24
30
  # Postgres's own `hecks_saga_instances` keeps an explicit `domain`
25
31
  # column under schema isolation (§3).
26
32
  #
33
+ # ## Record shape
34
+ #
27
35
  # One flat records hash, keyed by a composite string (Heki's own
28
36
  # snapshot format is id-keyed, not tuple-keyed) — never exposed
29
37
  # outside this class; `each_saga` yields the five real fields a
30
38
  # caller actually wants, not the internal key shape.
31
39
  #
40
+ # ## Locking
41
+ #
32
42
  # Locked the same way an aggregate's own store is: `with_lock`
33
43
  # (`Snapshot`, shared) serializes each save/delete's read-modify-
34
44
  # write against `@path`'s own lock file — a saga gets exactly the
@@ -38,12 +48,27 @@ module Hecks
38
48
  include Snapshot
39
49
  include Journal
40
50
 
51
+ # @param dir [String] the directory to hold the saga snapshot+journal file pair,
52
+ # shared with the aggregate `.heki` files that already live there
41
53
  def initialize(dir)
42
54
  @path = File.join(dir, "hecks_saga_instances.heki")
43
55
  @journal_path = "#{@path}.journal"
44
56
  @entry_mirrors = nil
45
57
  end
46
58
 
59
+ # Upserts one saga instance's checkpoint, keyed by domain, process manager and
60
+ # correlation, under the file lock.
61
+ #
62
+ # @param domain [String] the owning domain, carried in the record and filtered on
63
+ # read
64
+ # @param process_manager [String] the process manager's name
65
+ # @param correlation [String] the instance's correlation value
66
+ # @param state [String] the saga's current state name
67
+ # @param memory [Hash] the saga's working memory to persist
68
+ # @param completed_compensations [Array] the ledger of completed compensable legs;
69
+ # `[]` when none
70
+ # @return [Hash{String => Hash}] the store's full internal records Hash after the
71
+ # write; callers ignore it
47
72
  def save_saga(domain, process_manager, correlation, state, memory, completed_compensations = [])
48
73
  key = key_for(domain, process_manager, correlation)
49
74
  record = { "domain" => domain, "process_manager" => process_manager,
@@ -59,6 +84,14 @@ module Hecks
59
84
  end
60
85
  end
61
86
 
87
+ # Removes a finished saga instance's checkpoint, if present, under the file lock; a
88
+ # missing one is not an error.
89
+ #
90
+ # @param domain [String] the owning domain
91
+ # @param process_manager [String] the process manager's name
92
+ # @param correlation [String] the instance's correlation value
93
+ # @return [Hash{String => Hash}] the store's full internal records Hash after the
94
+ # delete; callers ignore it
62
95
  def delete_saga(domain, process_manager, correlation)
63
96
  key = key_for(domain, process_manager, correlation)
64
97
 
@@ -71,6 +104,19 @@ module Hecks
71
104
  end
72
105
  end
73
106
 
107
+ # Yields every checkpointed saga instance of `domain`, for `Registry
108
+ # #rehydrate_sagas!` to restore at boot.
109
+ #
110
+ # @param domain [String] the owning domain to filter records to
111
+ # @yieldparam process_manager [String] the process manager's name
112
+ # @yieldparam correlation [String] the instance's correlation value
113
+ # @yieldparam state [String] the saga's state name
114
+ # @yieldparam memory [Hash{Symbol => Object}] the saga's memory, Symbol keys at every
115
+ # depth
116
+ # @yieldparam completed_compensations [Array] the completed-compensation ledger, `[]`
117
+ # when the record carries none
118
+ # @return [Enumerator, Hash{String => Hash}] an enumerator over the same five values
119
+ # when no block is given; otherwise the store's full internal records Hash
74
120
  def each_saga(domain)
75
121
  return enum_for(:each_saga, domain) unless block_given?
76
122
 
@@ -23,7 +23,7 @@ module Hecks
23
23
  raise Malformed, "#{@path}: json error: #{e.message}"
24
24
  end
25
25
 
26
- # Temp-file-plus-rename, fsynced before the rename: a reader can
26
+ # Temp-file-plus-rename, fsynced ahead of the `File.rename` call: a reader can
27
27
  # only ever see the last complete snapshot or the one before it,
28
28
  # never a truncated or partial one — `File.binwrite`'s old
29
29
  # truncate-then-write left a window, proportional to the whole