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
@@ -25,6 +25,12 @@ module Hecks
25
25
 
26
26
  attr_reader :aggregate, :path, :events
27
27
 
28
+ # @param aggregate [Bluebook::Aggregate] the aggregate this store persists
29
+ # @param settings [Hash] adapter settings; `dir:`/`"dir"` (a storage directory, `"data"`
30
+ # by default) and `domain:`/`"domain"` (the saga-persistence scope, defaulting to
31
+ # `aggregate.name`) are read
32
+ # @param root [String, nil] the directory `settings[:dir]` resolves relative to when it
33
+ # is not absolute; defaults to the process's current working directory
28
34
  def initialize(aggregate:, settings: {}, root: nil)
29
35
  @aggregate = aggregate
30
36
  @path = resolve_path(settings, root)
@@ -47,6 +53,10 @@ module Hecks
47
53
  FileUtils.mkdir_p(File.dirname(@path))
48
54
  end
49
55
 
56
+ # Reads one record's current projected state.
57
+ #
58
+ # @param id [String, Object] the record's identity, compared as `id.to_s`
59
+ # @return [Runtime::Instance, nil] the stored record, or nil when no record has that id
50
60
  def find(id)
51
61
  record = store[id.to_s]
52
62
  return nil unless record
@@ -54,13 +64,23 @@ module Hecks
54
64
  instance(id.to_s, record)
55
65
  end
56
66
 
67
+ # Lists every record currently projected, sorted by id then reordered as requested.
68
+ #
69
+ # @param order_by [String, Symbol, nil] an attribute name to sort by; nil keeps id order
70
+ # @param direction [Symbol] `:asc` or `:desc`
71
+ # @return [Array<Runtime::Instance>] the stored records; `[]` when there are none
57
72
  def all(order_by: nil, direction: :asc)
58
73
  records = store.sort_by { |id, _| id }.map { |id, record| instance(id, record) }
59
74
  InMemoryOrdering.ordered(records, aggregate: @aggregate, order_by: order_by, direction: direction)
60
75
  end
61
76
 
77
+ # Counts the records currently projected.
78
+ #
79
+ # @return [Integer] number of stored records, not of journal entries
62
80
  def count = store.size
63
81
 
82
+ # Answers a declared query specification against the projected records.
83
+ #
64
84
  # `registry: context[:registry]` — Memory's own `query` already
65
85
  # threads this through; Heki's own never did, which made
66
86
  # `none_in_state?` (Ports::Query::InMemory) unconditionally
@@ -68,10 +88,21 @@ module Hecks
68
88
  # the target up" default) for every `none_in_state` where-clause
69
89
  # against a Heki-backed aggregate — silently excluding nothing,
70
90
  # always, no matter the actual target state.
91
+ #
92
+ # @param specification [QuerySpecification::Common::Options,
93
+ # Bluebook::Behaviour::ReadModel::FilteredOptions] the declared query specification
94
+ # @param args [Hash{Symbol => Object}] bound values for the specification's placeholders
95
+ # @param context [Hash{Symbol => Object}] call context; `:registry` is read and passed
96
+ # through for registry-aware comparisons
97
+ # @return [Array<Runtime::Instance>] the matching records, ordered and paged
71
98
  def query(specification, args = {}, context: {})
72
99
  Ports::Query::InMemory.execute(all, specification, args, registry: context[:registry])
73
100
  end
74
101
 
102
+ # Writes one entry to the durable journal, before any projection of it.
103
+ #
104
+ # @param entry [Persistence::Entry] the save or delete to journal
105
+ # @return [Persistence::Entry] `entry`, unchanged
75
106
  def append(entry)
76
107
  @entry_mirrors = entry.mirrors
77
108
  append_entry(entry.operation, entry.id, entry.state)
@@ -80,22 +111,42 @@ module Hecks
80
111
  @entry_mirrors = nil
81
112
  end
82
113
 
114
+ # Applies one journaled entry to the current-state snapshot.
115
+ #
83
116
  # Reads fresh rather than trusting the memoized `store` — under
84
117
  # `with_lock`, another process may have projected a snapshot since
85
118
  # this one last read it, and mutating *its* stale copy would
86
119
  # overwrite that write on disk rather than layer on top of it.
120
+ #
121
+ # On a delete, though, the record `current.delete` finds is already
122
+ # gone: every caller (`#delete` above, `AppendOnly#delete`) appends
123
+ # before it projects, so the fresh read above has already replayed
124
+ # this very entry off the journal. What the memoized `store` last
125
+ # held — from before this call — is read up front, for the return
126
+ # value only; it plays no part in what gets written.
127
+ #
128
+ # @param entry [Persistence::Entry] the save or delete to materialize
129
+ # @return [Runtime::Instance, nil] on a save, the newly stored record; on a delete, the
130
+ # removed record, or nil when no record had that id
87
131
  def project(entry)
132
+ removed = entry.delete? ? store[entry.id] : nil
88
133
  current = read
89
134
  if entry.save?
90
135
  current[entry.id] = Ports::Persistence::StateCodec.encode(@aggregate, entry.state)
136
+ projected = Runtime::Instance.new(aggregate: @aggregate, id: entry.id, state: entry.state)
91
137
  else
92
138
  current.delete(entry.id)
139
+ projected = removed && instance(entry.id, removed)
93
140
  end
94
141
  write(current)
95
142
  @store = current
96
- entry
143
+ projected
97
144
  end
98
145
 
146
+ # Journals and projects an instance's state under the file lock.
147
+ #
148
+ # @param instance [Runtime::Instance] the record to persist
149
+ # @return [Runtime::Instance] `instance`, unchanged
99
150
  def save(instance)
100
151
  entry = Ports::Persistence::Entry.new(operation: "save", id: instance.id.to_s, state: instance.state.dup)
101
152
  with_lock do
@@ -105,6 +156,11 @@ module Hecks
105
156
  instance
106
157
  end
107
158
 
159
+ # Journals and projects the removal of one record, if it exists, under the file lock.
160
+ #
161
+ # @param id [String, Object] the record's identity, compared as `id.to_s`
162
+ # @return [Boolean] true when a record was found and deleted, false when there was none
163
+ # and nothing was journaled
108
164
  def delete(id)
109
165
  return false unless find(id)
110
166
 
@@ -116,20 +172,55 @@ module Hecks
116
172
  true
117
173
  end
118
174
 
175
+ # Records one emitted event in this adapter's in-memory event log.
176
+ #
177
+ # @param event [Runtime::Event] the event to record
178
+ # @return [Array<Runtime::Event>] the adapter's in-memory event log, including `event`
119
179
  def record_event(event) = @events << event
120
180
 
121
181
  # ── the optional saga-persistence capability (§2) — Heki's own
122
182
  # shape (a sibling snapshot+journal file pair, `SagaStore`,
123
183
  # heki/saga_store.rb) rather than a table in a store this adapter
124
184
  # doesn't have.
185
+ #
186
+ # @param process_manager [String, Symbol] the process manager's name, compared as
187
+ # `.to_s`
188
+ # @param correlation [String, Symbol, Object] the instance's correlation value, compared
189
+ # as `.to_s`
190
+ # @param state [String, Symbol] the saga's current state name, compared as `.to_s`
191
+ # @param memory [Hash] the saga's working memory to persist
192
+ # @param completed_compensations [Array] the ledger of completed compensable legs;
193
+ # `[]` when none
194
+ # @return [Hash{String => Hash}] `SagaStore`'s internal records Hash after the write;
195
+ # callers ignore it
125
196
  def save_saga(process_manager:, correlation:, state:, memory:, completed_compensations: [])
126
197
  saga_store.save_saga(@domain, process_manager.to_s, correlation.to_s, state.to_s, memory, completed_compensations)
127
198
  end
128
199
 
200
+ # Removes a finished saga instance's checkpoint; a missing one is not an error.
201
+ #
202
+ # @param process_manager [String, Symbol] the process manager's name, compared as
203
+ # `.to_s`
204
+ # @param correlation [String, Symbol, Object] the instance's correlation value, compared
205
+ # as `.to_s`
206
+ # @return [Hash{String => Hash}] `SagaStore`'s internal records Hash after the delete;
207
+ # callers ignore it
129
208
  def delete_saga(process_manager:, correlation:)
130
209
  saga_store.delete_saga(@domain, process_manager.to_s, correlation.to_s)
131
210
  end
132
211
 
212
+ # Yields every checkpointed saga instance of this domain, for `Registry
213
+ # #rehydrate_sagas!` to restore at boot.
214
+ #
215
+ # @yieldparam process_manager [String] the process manager's name
216
+ # @yieldparam correlation [String] the instance's correlation value
217
+ # @yieldparam state [String] the saga's state name
218
+ # @yieldparam memory [Hash{Symbol => Object}] the saga's memory, Symbol keys at every
219
+ # depth
220
+ # @yieldparam completed_compensations [Array] the completed-compensation ledger, `[]`
221
+ # when none was recorded
222
+ # @return [Enumerator, Hash{String => Hash}] an enumerator over the same five values
223
+ # when no block is given; otherwise `SagaStore`'s internal records Hash
133
224
  def each_saga(&) = saga_store.each_saga(@domain, &)
134
225
 
135
226
  private
@@ -159,7 +250,7 @@ module Hecks
159
250
 
160
251
  # `dir: :default` — a bare Symbol, the framework's own convention
161
252
  # for "a declared value that resolves by convention, never a silent
162
- # fallback" — used to crash `File.join` outright
253
+ # fallback" — would otherwise crash `File.join` outright
163
254
  # (`TypeError: no implicit conversion of Symbol into String`):
164
255
  # `resolve_path` only ever checked for a missing `dir` setting,
165
256
  # never a Symbol one. Treated the same as no setting at all — falls
@@ -8,11 +8,21 @@ module Hecks
8
8
  module IdentityRegistry
9
9
  module_function
10
10
 
11
+ # Looks up the id of the identity an authenticated (issuer, subject) pair is linked to,
12
+ # querying the Identity framework bluebook's own `ResolvedBy`.
13
+ #
11
14
  # `nil` for a pair nothing has linked, the first match otherwise —
12
15
  # `ResolvedBy` is a lookup by the exact (issuer, subject) an
13
16
  # authenticated token carries, not a listing, so more than one row
14
17
  # would mean two links share a pair, which `Link`'s own natural
15
18
  # key already prevents by construction.
19
+ #
20
+ # @param registry [Runtime::Registry] the booted registry, queried for the linked
21
+ # identity's id
22
+ # @param issuer [String] the OIDC issuer that authenticated the caller, compared as a
23
+ # String
24
+ # @param subject [String] the OIDC subject the issuer vouches for, compared as a String
25
+ # @return [String, nil] the linked identity's id, or nil if nothing has linked this pair
16
26
  def resolve(registry, issuer:, subject:)
17
27
  rows = Runtime::Dispatcher.new(registry).query(
18
28
  "Identity::ExternalIdentifier.ResolvedBy",
@@ -16,10 +16,24 @@ module Hecks
16
16
  module InMemoryOrdering
17
17
  module_function
18
18
 
19
+ # Orders decoded records by a declared attribute, then by id as the total-order
20
+ # tiebreaker.
21
+ #
19
22
  # order_by is a runtime value (an HTTP query param, in the
20
23
  # console's case), not framework-authored bluebook source — see
21
24
  # postgres.rb's own all for the full reasoning. Whitelisted the
22
25
  # identical way before FieldPath.dig ever runs.
26
+ #
27
+ # @param records [Array<Runtime::Instance>] the records to order
28
+ # @param aggregate [Bluebook::Aggregate] the aggregate `records` belong to, checked for
29
+ # `order_by`'s attribute
30
+ # @param order_by [String, Symbol, nil] a dotted attribute path to sort by; nil returns
31
+ # `records` unchanged
32
+ # @param direction [Symbol] `:asc` or `:desc`
33
+ # @return [Array<Runtime::Instance>] `records`, ordered by `order_by` then id; unchanged
34
+ # when `order_by` is nil
35
+ # @raise [Runtime::WiringError] if `order_by` names no attribute of `aggregate` and is
36
+ # not its lifecycle field
23
37
  def ordered(records, aggregate:, order_by:, direction:)
24
38
  return records unless order_by
25
39
 
@@ -36,6 +50,14 @@ module Hecks
36
50
  end
37
51
  end
38
52
 
53
+ # Resolves the dotted path `FieldPath.dig` should read to compare a value object field.
54
+ #
55
+ # @param aggregate [Bluebook::Aggregate] the aggregate `field` belongs to
56
+ # @param field [String, Symbol] a dotted order_by path, such as `"price"` or
57
+ # `"price.cents"`
58
+ # @return [String] `field` unchanged for an already-dotted path, the lifecycle field, or
59
+ # an attribute with no value-object type; otherwise `"<name>.<member>"` naming the
60
+ # attribute's numeric member, its sole member, or the bare `"value"` convention
39
61
  def sortable_path(aggregate, field)
40
62
  name, *path = field.to_s.split(".")
41
63
  return field.to_s unless path.empty?
@@ -0,0 +1,3 @@
1
+ Hecks.adapter "InProcessKeyVault" do
2
+ port "key_vault"
3
+ end
@@ -0,0 +1,53 @@
1
+ require "securerandom"
2
+
3
+ module Hecks
4
+ module Adapters
5
+ # The real `key_vault` fulfillment for a single running process — a
6
+ # table of key material, keyed by an opaque reference never exposed to
7
+ # a bluebook or an event. Cryptoshredding only works if the key
8
+ # genuinely leaves memory when destroyed: `destroy` deletes the Hash
9
+ # entry outright, not merely a flag, so a `fetch` afterward has
10
+ # nothing left to decrypt with.
11
+ #
12
+ # **Not crash-durable** — a process restart loses every key this
13
+ # adapter ever issued, along with every ciphertext under it. A
14
+ # deployment that needs destruction to survive a restart (or to leave
15
+ # a physical-destruction audit trail) backs this port with a real
16
+ # `KMS` or `HSM` instead; this default asks for nothing external to
17
+ # run, the same tradeoff `SecureRandomIdentity` already makes for
18
+ # identity minting.
19
+ module InProcessKeyVault
20
+ module_function
21
+
22
+ # Mints and stores a fresh symmetric key, returning only an opaque handle to it.
23
+ #
24
+ # @param subject_id [String] the data subject the key is being issued for; held
25
+ # alongside the key material for {#destroy}'s own bookkeeping, never returned
26
+ # @return [String] an opaque key reference; the key material itself never leaves this
27
+ # adapter
28
+ def issue(subject_id:)
29
+ key_reference = SecureRandom.uuid
30
+ (@keys ||= {})[key_reference] = { subject_id: subject_id, secret: SecureRandom.hex(32) }
31
+ key_reference
32
+ end
33
+
34
+ # Looks up the live key material behind a reference, for encrypting or decrypting.
35
+ #
36
+ # @param key_reference [String] the opaque reference {#issue} returned
37
+ # @return [String, nil] the key's hex-encoded secret, or nil once {#destroy} has run
38
+ def fetch(key_reference) = (@keys ||= {})[key_reference]&.fetch(:secret)
39
+
40
+ # Irrevocably deletes a key's material, so {#fetch} can never answer for it again.
41
+ #
42
+ # @param key_reference [String] the opaque reference {#issue} returned
43
+ # @return [Boolean] true when a key was held and is now gone; false when this
44
+ # reference was already destroyed, or never issued
45
+ def destroy(key_reference:) = !(@keys ||= {}).delete(key_reference).nil? # rubocop:disable Naming/PredicateMethod
46
+
47
+ # Resets the vault to empty, forgetting every key this process ever issued.
48
+ #
49
+ # @return [void]
50
+ def reset! = @keys = {}
51
+ end
52
+ end
53
+ end
@@ -9,6 +9,8 @@ module Hecks
9
9
  # domain-specific: neither caller needs to know an AWS SDK is
10
10
  # involved at all.
11
11
  #
12
+ # ## Resolving the function name
13
+ #
12
14
  # Function name is computed unless it is named — `"hecks-#{domain}"`,
13
15
  # lowercased, matches bin/project_deploy's own `stack_name`
14
16
  # exactly (bin/project_deploy: `stack_name = "hecks-#{domain_name}"`,
@@ -19,6 +21,8 @@ module Hecks
19
21
  # reproduces the same string bin/project_deploy computes from the
20
22
  # directory.
21
23
  #
24
+ # ## When the assumption breaks
25
+ #
22
26
  # That assumption is not always true, and when it breaks nothing
23
27
  # about it is recoverable from here. `bin/project_deploy` honours
24
28
  # a `.world`'s own `stack_prefix`/`stack_name` — settings that
@@ -33,6 +37,8 @@ module Hecks
33
37
  # that does not exist — found live, and visible in the deployed
34
38
  # journal having never received a single row.
35
39
  #
40
+ # ## Naming it explicitly
41
+ #
36
42
  # So the name can be named, in the one place the rest of this
37
43
  # deployment is already described: the `.world`'s own
38
44
  # `persisted_by("Lambda")`/`dispatched_by("Lambda")` block, beside
@@ -40,6 +46,12 @@ module Hecks
40
46
  # is unchanged, which is every domain whose stack name was never
41
47
  # pinned.
42
48
  class Client
49
+ # @param domain [String, Symbol] the bluebook's own declared domain name; computes
50
+ # the function name when `function` is not given
51
+ # @param region [String] the AWS region to invoke in
52
+ # @param function [String, Symbol, nil] an explicit Lambda function name, from a
53
+ # `.world`'s `persisted_by("Lambda")`/`dispatched_by("Lambda")` block; nil computes
54
+ # `"hecks-#{domain.downcase}"`
43
55
  def initialize(domain:, region:, function: nil)
44
56
  require "aws-sdk-lambda"
45
57
  @function_name = function.to_s.empty? ? "hecks-#{domain.to_s.downcase}" : function.to_s
@@ -48,21 +60,37 @@ module Hecks
48
60
 
49
61
  # The function this client actually invokes — read by
50
62
  # `Runtime::WiringError` messages and worth asserting on
51
- # directly, since "which function did we call" is precisely the
52
- # thing that used to be unanswerable from outside.
63
+ # directly, since "which function did we call" is otherwise
64
+ # unanswerable from outside.
53
65
  attr_reader :function_name
54
66
 
67
+ # Reads the whole domain's current state and event log from the remote function.
68
+ #
55
69
  # **The whole domain, every time** — matches dispatch::read's own
56
70
  # rehydrate-the-full-journal design (Phase 1, rust/host). No
57
- # caching here, deliberately not even per-request: Lambda#all
58
- # used to memoize this across calls, which silently served
59
- # stale reads for a warm web process's whole lifetime once a
60
- # write happened elsewhere — see Lambda#instances's own comment
61
- # on the real, live bug that caught it.
71
+ # caching here, deliberately: memoizing this across calls once
72
+ # silently served stale reads for a warm web process's whole
73
+ # lifetime after a write happened elsewhere — see Lambda#instances's
74
+ # own comment on the real, live bug that caught it.
75
+ #
76
+ # @return [Hash{String => Object}] the parsed JSON response; the deployed function's
77
+ # own top-level keys (`"instances"`, `"events"`, …)
78
+ # @raise [Runtime::WiringError] if the function reports a `functionError` (see
79
+ # `invoke`)
62
80
  def read
63
81
  invoke({ "read" => true })
64
82
  end
65
83
 
84
+ # Dispatches one command to the remote function and returns its parsed response.
85
+ #
86
+ # @param verb [String] the fully-qualified verb to dispatch
87
+ # @param args [Hash] the command's declared arguments
88
+ # @param role [String, Symbol, nil] the role to dispatch as; omitted from the payload
89
+ # when nil
90
+ # @return [Hash{String => Object}] the parsed JSON response, including a `"refusals"`
91
+ # array and a `"mutations"` array
92
+ # @raise [Runtime::WiringError] if the function reports a `functionError` (see
93
+ # `invoke`)
66
94
  def dispatch(verb, args, role: nil)
67
95
  payload = { "verb" => verb, "args" => args }
68
96
  payload["role"] = role if role
@@ -29,6 +29,16 @@ module Hecks
29
29
 
30
30
  attr_reader :aggregate
31
31
 
32
+ # Resolves which Lambda function's Store to read from and builds the client that
33
+ # reads it.
34
+ #
35
+ # @param aggregate [Bluebook::Aggregate] the aggregate whose records this adapter reads
36
+ # @param settings [Hash{Symbol, String => Object}] world settings for the binding:
37
+ # `domain` (prefixes the instances lookup; default the aggregate's own name),
38
+ # `region` (default `"us-east-1"`) and `function` (the function name, when it is not
39
+ # `hecks-<domain>`), each read under a Symbol or a String key
40
+ # @param root [String, nil] the boot's own project directory, used with `DOMAIN_NAME`
41
+ # to resolve which function to call; nil falls further back to `domain`
32
42
  def initialize(aggregate:, settings: {}, root: nil)
33
43
  @aggregate = aggregate
34
44
  domain =
@@ -80,16 +90,40 @@ module Hecks
80
90
  @prefix = "#{domain}::#{aggregate.hecks_name}#"
81
91
  end
82
92
 
93
+ # Looks up the current record for one aggregate identity, reading the Lambda's own
94
+ # Store fresh on every call.
95
+ #
96
+ # @param id [String, Object] the aggregate identity, compared as `id.to_s`
97
+ # @return [Runtime::Instance, nil] the decoded record, or nil when no record has that id
83
98
  def find(id)
84
99
  instances[id.to_s]
85
100
  end
86
101
 
102
+ # Lists every stored record, in id order unless an ordering attribute is given.
103
+ #
104
+ # @param order_by [String, Symbol, nil] attribute (or dotted value-object path) to sort
105
+ # by; nil orders by id alone
106
+ # @param direction [Symbol, String] `:asc` or `:desc`
107
+ # @return [Array<Runtime::Instance>] the decoded records, `[]` when the Store holds none
108
+ # with this aggregate's own prefix
109
+ # @raise [Runtime::WiringError] if `order_by` names no attribute of the aggregate
87
110
  def all(order_by: nil, direction: :asc)
88
111
  InMemoryOrdering.ordered(instances.values, aggregate: @aggregate, order_by: order_by, direction: direction)
89
112
  end
90
113
 
114
+ # Counts the records currently held for this aggregate.
115
+ #
116
+ # @return [Integer] number of records
91
117
  def count = instances.size
92
118
 
119
+ # Answers a declared query by filtering, ordering and paging the held records in Ruby.
120
+ #
121
+ # @param specification [QuerySpecification::Common::Options] the declared query
122
+ # @param args [Hash{Symbol => Object}] values for the specification's symbolic operands
123
+ # @param context [Hash] execution context from `Ports::Query.execute`; accepted for the
124
+ # port's call shape and not read
125
+ # @return [Array<Runtime::Instance>] the matching records, `[]` when none match
126
+ # @raise [Runtime::WiringError] if a where clause uses an operation no comparator handles
93
127
  def query(specification, args = {}, context: {})
94
128
  Ports::Query::InMemory.execute(instances.values, specification, args)
95
129
  end
@@ -103,10 +137,17 @@ module Hecks
103
137
  # 1 — there is no local write-ahead log for `recover!` to replay),
104
138
  # `append`/`project` raise rather than silently no-op.
105
139
 
140
+ # Reads a world setting under either a Symbol or a String key, with a fallback.
141
+ #
106
142
  # A `.world` block's settings arrive symbol-keyed from the DSL and
107
143
  # string-keyed from a round-tripped export, so every read has to
108
144
  # accept both — one helper rather than the same five lines per
109
145
  # key.
146
+ #
147
+ # @param settings [Hash] the world settings Hash to read from
148
+ # @param key [Symbol] the setting name, tried as itself and as `key.to_s`
149
+ # @param fallback [Object] the value to return when neither key is present
150
+ # @return [Object] the setting's value, or `fallback` when absent
110
151
  def setting(settings, key, fallback)
111
152
  return settings[key] if settings.key?(key)
112
153
  return settings[key.to_s] if settings.key?(key.to_s)
@@ -121,8 +162,8 @@ module Hecks
121
162
  # held by the registry `RUNTIME = Hecks.boot(...)` builds once
122
163
  # per Lambda web process — WebFunction's own top-level constant,
123
164
  # reused warm across every HTTP request that process serves, not
124
- # rebuilt per request the way a memoize-for-one-request comment
125
- # here used to assume). A real, live bug caught this: a mutation
165
+ # rebuilt per request the way memoizing here would wrongly assume).
166
+ # A real, live bug caught this: a mutation
126
167
  # dispatched fine (RemoteDispatcher always calls the dispatch
127
168
  # Lambda fresh) and the very next `.all` on the same warm
128
169
  # container kept returning the state from before that mutation,
@@ -35,11 +35,20 @@ module Hecks
35
35
  # browser tab is exactly one origin, exactly one user; there is no
36
36
  # second tenant this in-process Hash could ever confuse a first
37
37
  # one with.
38
+ #
39
+ # @return [Boolean] true, always
38
40
  def self.tenant_capable? = true
41
+
42
+ # Lists the optional persistence behaviours this adapter advertises.
43
+ #
44
+ # @return [Array<Symbol>] `[:atomic_put]`
39
45
  def persistence_capabilities = [:atomic_put]
40
46
 
41
47
  attr_reader :aggregate, :events
42
48
 
49
+ # @param aggregate [Bluebook::Aggregate] the aggregate this store persists
50
+ # @param settings [Hash] accepted but not read by this class
51
+ # @param root [String, nil] accepted but not read by this class
43
52
  def initialize(aggregate:, settings: {}, root: nil)
44
53
  @aggregate = aggregate
45
54
  @records = {}
@@ -47,22 +56,48 @@ module Hecks
47
56
  @entries = []
48
57
  end
49
58
 
59
+ # Reads one record's current projected state.
60
+ #
61
+ # @param id [String, Object] the record's identity, compared as `id.to_s`
62
+ # @return [Runtime::Instance, nil] the stored record, or nil when no record has that id
50
63
  def find(id) = @records[id.to_s]
64
+
65
+ # Counts the records currently projected.
66
+ #
67
+ # @return [Integer] number of stored records
51
68
  def count = @records.size
52
69
 
70
+ # Lists every record currently projected.
71
+ #
72
+ # @param order_by [String, Symbol, nil] an attribute name to sort by; nil keeps
73
+ # insertion order
74
+ # @param direction [Symbol] `:asc` or `:desc`
75
+ # @return [Array<Runtime::Instance>] the stored records; `[]` when there are none
53
76
  def all(order_by: nil, direction: :asc)
54
77
  InMemoryOrdering.ordered(@records.values, aggregate: @aggregate, order_by: order_by, direction: direction)
55
78
  end
56
79
 
80
+ # Answers a declared query specification against the projected records.
81
+ #
57
82
  # The decision the guide asks for, made explicitly: no compiled
58
83
  # dialect of its own, same as Heki/Memory — a personal-scale local
59
84
  # store answering by walking `all` is correct on day one, and
60
85
  # nothing about a browser tab's own data volume asks for pushdown.
86
+ #
87
+ # @param specification [QuerySpecification::Common::Options,
88
+ # Bluebook::Behaviour::ReadModel::FilteredOptions] the declared query specification
89
+ # @param args [Hash{Symbol => Object}] bound values for the specification's placeholders
90
+ # @param context [Hash{Symbol => Object}] call context; `:registry` is read and passed
91
+ # through for registry-aware comparisons
92
+ # @return [Array<Runtime::Instance>] the matching records, ordered and paged
61
93
  def query(specification, args = {}, context: {})
62
94
  Ports::Query::InMemory.execute(all, specification, args, registry: context[:registry])
63
95
  end
64
96
 
65
- # Through the state codec, the same as Memory (see its `append`).
97
+ # Records one journal entry, holding a codec copy of its state, the same as `Memory#append`.
98
+ #
99
+ # @param entry [Ports::Persistence::Entry] the save or delete to journal
100
+ # @return [Ports::Persistence::Entry] the caller's own `entry`, not the journalled copy
66
101
  def append(entry)
67
102
  copied = Ports::Persistence::Entry.new(operation: entry.operation, id: entry.id,
68
103
  state: copy(entry.state), mirrors: entry.mirrors)
@@ -70,6 +105,11 @@ module Hecks
70
105
  entry
71
106
  end
72
107
 
108
+ # Applies one journaled entry to the current-state store.
109
+ #
110
+ # @param entry [Persistence::Entry] the save or delete to materialize
111
+ # @return [Runtime::Instance, nil] on a save, the newly stored record; on a delete, the
112
+ # removed record, or nil when no record had that id
73
113
  def project(entry)
74
114
  if entry.save?
75
115
  @records[entry.id] = Runtime::Instance.new(aggregate: @aggregate, id: entry.id, state: copy(entry.state))
@@ -78,12 +118,24 @@ module Hecks
78
118
  end
79
119
  end
80
120
 
121
+ # Journals and projects an instance's state.
122
+ #
123
+ # @param instance [Runtime::Instance] the record to persist
124
+ # @return [Runtime::Instance] the newly stored record (a copy of `instance`'s state)
81
125
  def save(instance)
82
126
  entry = Ports::Persistence::Entry.new(operation: "save", id: instance.id.to_s, state: copy(instance.state))
83
127
  append(entry)
84
128
  project(entry)
85
129
  end
86
130
 
131
+ # Writes an entry through an existence check, refusing to replace an existing record
132
+ # when asked.
133
+ #
134
+ # @param entry [Persistence::Entry] the entry to persist (its `id` checked for an
135
+ # existing record)
136
+ # @param insert_only [Boolean] true to refuse replacing a record that already exists
137
+ # @return [Symbol] `:conflicted` when `insert_only` met an existing record and nothing
138
+ # was written; `:inserted` or `:replaced` otherwise
87
139
  def atomic_put(entry, insert_only: false)
88
140
  exists = @records.key?(entry.id.to_s)
89
141
  return :conflicted if insert_only && exists
@@ -94,16 +146,30 @@ module Hecks
94
146
  status
95
147
  end
96
148
 
149
+ # Journals and projects the removal of one record.
150
+ #
151
+ # @param id [String, Object] the record's identity, compared as `id.to_s`
152
+ # @return [Runtime::Instance, nil] the removed record, or nil when no record had that id
97
153
  def delete(id)
98
154
  entry = Ports::Persistence::Entry.new(operation: "delete", id: id.to_s, state: nil)
99
155
  append(entry)
100
156
  project(entry)
101
157
  end
102
158
 
159
+ # Records one emitted event in this adapter's in-memory event log.
160
+ #
161
+ # @param event [Runtime::Event] the event to record
162
+ # @return [Array<Runtime::Event>] the adapter's in-memory event log, including `event`
103
163
  def record_event(event) = @events << event
104
164
 
165
+ # Reads the whole journal, oldest entry first.
166
+ #
167
+ # @return [Array<Persistence::Entry>] a shallow copy of every appended entry
105
168
  def entries = @entries.dup
106
169
 
170
+ # Clears the stored records, events and journal so a kept runtime starts clean.
171
+ #
172
+ # @return [LocalStorage] self, so a caller can chain after resetting
107
173
  def reset!
108
174
  @records = {}
109
175
  @events = []