hecks 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) 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.hecksagon +19 -0
  112. data/lib/hecks/doc/reference.rb +185 -16
  113. data/lib/hecks/embryonaut_bluebook.rb +32 -9
  114. data/lib/hecks/facade/handle.rb +76 -3
  115. data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
  116. data/lib/hecks/forms/field_shape.rb +3 -0
  117. data/lib/hecks/forms/page.rb +14 -0
  118. data/lib/hecks/forms/port_argument.rb +12 -0
  119. data/lib/hecks/forms/query_form_renderer.rb +63 -0
  120. data/lib/hecks/forms/record_renderer.rb +58 -0
  121. data/lib/hecks/forms/record_table.rb +27 -0
  122. data/lib/hecks/forms/reference_options.rb +24 -0
  123. data/lib/hecks/forms/value_object_shape.rb +10 -0
  124. data/lib/hecks/fqn.rb +58 -0
  125. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  126. data/lib/hecks/framework/oidc.json +15 -0
  127. data/lib/hecks/framework.rb +43 -20
  128. data/lib/hecks/freezer.rb +17 -1
  129. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
  130. data/lib/hecks/fuzzing/combination_miner.rb +59 -0
  131. data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
  132. data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
  133. data/lib/hecks/fuzzing/differential.rb +34 -0
  134. data/lib/hecks/fuzzing/domain_generator.rb +188 -11
  135. data/lib/hecks/fuzzing/era_boundary.rb +45 -15
  136. data/lib/hecks/fuzzing/form_census.rb +86 -0
  137. data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
  138. data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
  139. data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
  140. data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
  141. data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
  142. data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
  143. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
  144. data/lib/hecks/fuzzing/properties/guards.rb +44 -0
  145. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
  146. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
  147. data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
  148. data/lib/hecks/fuzzing/properties/querying.rb +68 -14
  149. data/lib/hecks/fuzzing/properties.rb +24 -15
  150. data/lib/hecks/fuzzing/qa_settings.rb +12 -0
  151. data/lib/hecks/fuzzing/replay.rb +137 -29
  152. data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
  153. data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
  154. data/lib/hecks/fuzzing/self_consistency.rb +189 -40
  155. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
  156. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
  157. data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
  158. data/lib/hecks/fuzzing/shrinker.rb +114 -2
  159. data/lib/hecks/fuzzing/structural_skips.rb +18 -4
  160. data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
  161. data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
  162. data/lib/hecks/fuzzing/value_generator.rb +98 -10
  163. data/lib/hecks/grammar/evolve.rb +178 -2
  164. data/lib/hecks/grammar.rb +46 -0
  165. data/lib/hecks/ir.rb +38 -7
  166. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  167. data/lib/hecks/literal.rb +32 -0
  168. data/lib/hecks/naming.rb +88 -7
  169. data/lib/hecks/ports/access_control.rb +5 -10
  170. data/lib/hecks/ports/authorization.rb +3 -6
  171. data/lib/hecks/ports/identity_assignment.rb +1 -2
  172. data/lib/hecks/ports/identity_resolution.rb +1 -2
  173. data/lib/hecks/ports/key_vault.port +6 -0
  174. data/lib/hecks/ports/key_vault.rb +58 -0
  175. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
  176. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
  177. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
  178. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
  179. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
  180. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  181. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
  182. data/lib/hecks/ports/query/in_memory.rb +39 -6
  183. data/lib/hecks/ports/query/ordering.rb +15 -0
  184. data/lib/hecks/ports.rb +1 -0
  185. data/lib/hecks/projections/bootstrap_table.rb +43 -8
  186. data/lib/hecks/projections/diagrams.rb +243 -7
  187. data/lib/hecks/projections/glossary/html.rb +88 -0
  188. data/lib/hecks/projections/glossary/markdown.rb +47 -0
  189. data/lib/hecks/projections/glossary/mermaid.rb +48 -0
  190. data/lib/hecks/projections/glossary/sentences.rb +105 -0
  191. data/lib/hecks/projections/glossary.rb +161 -19
  192. data/lib/hecks/projections/model/deviations.rb +44 -0
  193. data/lib/hecks/projections/model.rb +51 -1
  194. data/lib/hecks/projections/oidc.rb +40 -3
  195. data/lib/hecks/projections/parser_table.rb +49 -8
  196. data/lib/hecks/projections/reference.rb +12 -0
  197. data/lib/hecks/projections/rust_vocabulary.rb +219 -16
  198. data/lib/hecks/projections/shape.rb +8 -0
  199. data/lib/hecks/projections/statements.rb +63 -16
  200. data/lib/hecks/projections/vocabulary.rb +17 -0
  201. data/lib/hecks/projector/cli_projector.rb +218 -10
  202. data/lib/hecks/projector/docs_projector.rb +145 -19
  203. data/lib/hecks/projector/exporter.rb +65 -11
  204. data/lib/hecks/projector/ir_projector.rb +6 -0
  205. data/lib/hecks/projector/narrate_projector.rb +136 -15
  206. data/lib/hecks/projector/target.rb +29 -5
  207. data/lib/hecks/projector.rb +74 -6
  208. data/lib/hecks/query_ir.rb +47 -0
  209. data/lib/hecks/query_specification/common/null_policy.rb +5 -3
  210. data/lib/hecks/rendering.rb +6 -0
  211. data/lib/hecks/router/namespace_installer.rb +13 -0
  212. data/lib/hecks/router.rb +55 -0
  213. data/lib/hecks/runtime/aggregate_lock.rb +9 -0
  214. data/lib/hecks/runtime/boot_gates.rb +18 -0
  215. data/lib/hecks/runtime/caller.rb +32 -0
  216. data/lib/hecks/runtime/capability_graph.rb +11 -0
  217. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
  218. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
  219. data/lib/hecks/runtime/command_interpreter.rb +42 -17
  220. data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
  221. data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
  222. data/lib/hecks/runtime/command_rules/references.rb +118 -28
  223. data/lib/hecks/runtime/dependency_planning.rb +45 -0
  224. data/lib/hecks/runtime/dispatcher.rb +28 -50
  225. data/lib/hecks/runtime/entity_element.rb +161 -8
  226. data/lib/hecks/runtime/entity_interpreter.rb +44 -9
  227. data/lib/hecks/runtime/errors.rb +18 -4
  228. data/lib/hecks/runtime/event.rb +10 -5
  229. data/lib/hecks/runtime/identity.rb +71 -3
  230. data/lib/hecks/runtime/instance.rb +67 -7
  231. data/lib/hecks/runtime/interpreting.rb +13 -5
  232. data/lib/hecks/runtime/invocation.rb +118 -36
  233. data/lib/hecks/runtime/loader.rb +94 -8
  234. data/lib/hecks/runtime/outbox.rb +145 -7
  235. data/lib/hecks/runtime/policy_interpreter.rb +22 -9
  236. data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
  237. data/lib/hecks/runtime/query_interpreter.rb +40 -12
  238. data/lib/hecks/runtime/reaction_invocation.rb +53 -8
  239. data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
  240. data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
  241. data/lib/hecks/runtime/reference_hop.rb +42 -0
  242. data/lib/hecks/runtime/refusal_wording.rb +50 -0
  243. data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
  244. data/lib/hecks/runtime/registry/verification.rb +119 -4
  245. data/lib/hecks/runtime/registry.rb +157 -4
  246. data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
  247. data/lib/hecks/runtime/routing.rb +27 -2
  248. data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
  249. data/lib/hecks/runtime/saga_interpreter.rb +27 -13
  250. data/lib/hecks/runtime/tenant_check.rb +26 -6
  251. data/lib/hecks/runtime/tenant_scope.rb +18 -0
  252. data/lib/hecks/runtime/value/coercion.rb +255 -33
  253. data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
  254. data/lib/hecks/runtime/value.rb +50 -0
  255. data/lib/hecks/runtime.rb +32 -0
  256. data/lib/hecks/storehouse.rb +305 -9
  257. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  258. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  259. data/lib/hecks/version.rb +1 -1
  260. data/lib/hecks.rb +79 -1
  261. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
  262. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
  263. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
  264. metadata +11 -5
  265. data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
  266. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
  267. data/lib/hecks/deprecation.rb +0 -95
data/lib/hecks/naming.rb CHANGED
@@ -18,9 +18,19 @@ module Hecks
18
18
 
19
19
  module_function
20
20
 
21
- # The parts of an identity, joined in declaration order.
21
+ # Joins the parts of an identity in declaration order.
22
+ #
23
+ # @param parts [Array<#to_s>, #to_s] one or more identity segments; a bare
24
+ # value is wrapped in a single-element Array
25
+ # @return [String] the segments joined with `IDENTITY_JOIN`
22
26
  def identity(parts) = Array(parts).join(IDENTITY_JOIN)
23
27
 
28
+ # Strips a namespace path down to its last segment.
29
+ #
30
+ # @param type [Module, String, Symbol, #to_s] a `::`-joined constant path, or
31
+ # anything whose `to_s` is one
32
+ # @return [String] the text after the last `::`, or the whole `to_s` if there
33
+ # is none
24
34
  def demodulise(type)
25
35
  type.to_s.split("::").last.to_s
26
36
  end
@@ -28,10 +38,17 @@ module Hecks
28
38
  # snake_case -> PascalCase. The name a synthesised closed-set value object
29
39
  # takes when an attribute declares one inline. The derivation is part of
30
40
  # the IR contract: the same bluebook must always produce the same name.
41
+ #
42
+ # @param text [String, Symbol, #to_s] a snake_case (or already Pascal) name
43
+ # @return [String] the PascalCase form
31
44
  def pascal(text)
32
45
  text.to_s.split("_").map { |part| part.sub(/\A(.)/) { Regexp.last_match(1).upcase } }.join
33
46
  end
34
47
 
48
+ # PascalCase or camelCase -> snake_case.
49
+ #
50
+ # @param text [String, Symbol, #to_s] a Pascal-, camel-, or already snake-case name
51
+ # @return [String] the lowercase, underscore-separated form
35
52
  def snake(text)
36
53
  text.to_s
37
54
  .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
@@ -48,6 +65,9 @@ module Hecks
48
65
  # nobody says ("Atm"). First word capitalized, the rest lowercased,
49
66
  # so a headword reads as sentence case whatever casing it was
50
67
  # declared in.
68
+ #
69
+ # @param text [String, Symbol, #to_s] a Pascal-, camel-, or snake-case identifier
70
+ # @return [String] the space-separated, sentence-cased phrase
51
71
  def words(text)
52
72
  parts = text.to_s
53
73
  .tr("_", " ")
@@ -61,9 +81,14 @@ module Hecks
61
81
  end.join(" ")
62
82
  end
63
83
 
64
- # "A, B, and C" / "A or B" — the Oxford-comma list every English
65
- # sentence a projection writes wants; lived in `NarrateProjector`
66
- # alone until a second projection needed it.
84
+ # Joins items into the Oxford-comma list — "A, B, and C" / "A or B" —
85
+ # every English sentence a projection writes wants; lived in
86
+ # `NarrateProjector` alone until a second projection needed it.
87
+ #
88
+ # @param items [Array<#to_s>] the items to join
89
+ # @param conj [String] the word before the last item, such as `"and"` or `"or"`
90
+ # @return [String] `""` for no items, the item's `to_s` for one, and the
91
+ # Oxford-comma join for more
67
92
  def to_sentence_list(items, conj: "and")
68
93
  case items.size
69
94
  when 0 then ""
@@ -73,9 +98,12 @@ module Hecks
73
98
  end
74
99
  end
75
100
 
76
- # The vowel-letter heuristic — safe here for the same reason
77
- # `Projections::Statements#article` gives: a construct name is a
78
- # plain word, never "hour" or "university".
101
+ # Picks the article for `word` by its first letter — the vowel-letter
102
+ # heuristic, safe here for the same reason `Projections::Statements#article`
103
+ # gives: a construct name is a plain word, never "hour" or "university".
104
+ #
105
+ # @param word [String, Symbol, #to_s] the word the article precedes
106
+ # @return [String] `"an"` if `word` starts with a vowel letter, `"a"` otherwise
79
107
  def a_or_an(word)
80
108
  %w[a e i o u].include?(word.to_s[0].to_s.downcase) ? "an" : "a"
81
109
  end
@@ -89,6 +117,9 @@ module Hecks
89
117
  # the wrong rule against itself. Agreement is not correctness; it never was.
90
118
  #
91
119
  # So: one pluraliser, three rules, and every collection name flows through it.
120
+ #
121
+ # @param text [String, Symbol, #to_s] a singular name
122
+ # @return [String] the pluralised name
92
123
  def plural(text)
93
124
  word = text.to_s
94
125
  return "#{word[0..-2]}ies" if word.match?(/[^aeiou]y\z/)
@@ -104,6 +135,9 @@ module Hecks
104
135
  # already wrote as a real aggregate, so "ies -> y, trailing s dropped" is the
105
136
  # whole rule — enough for `has_many Invoices` to resolve to the aggregate
106
137
  # actually named Invoice.
138
+ #
139
+ # @param text [String, Symbol, #to_s] a plural name
140
+ # @return [String] the singularised name
107
141
  def singularize(text)
108
142
  word = text.to_s
109
143
  return "#{word[0..-4]}y" if word.length > 3 && word.end_with?("ies")
@@ -124,25 +158,47 @@ module Hecks
124
158
  word
125
159
  end
126
160
 
161
+ # Derives the attribute name a reference to `type` is stored under.
162
+ #
163
+ # @param type [Module, String, Symbol, #to_s] the referenced construct's name
164
+ # or a `::`-joined path to it
165
+ # @return [Symbol] the snake_case, demodulised name, as a Symbol
127
166
  def reference_key(type)
128
167
  snake(demodulise(type)).to_sym
129
168
  end
130
169
 
170
+ # Splits a `qualifier.name` string on its first dot.
171
+ #
172
+ # @param dotted [String, Symbol, #to_s] text, optionally containing a dot
173
+ # @return [Array(String, String)] `[before the first dot, after it]`; the
174
+ # second element is `""` when `dotted` has no dot
131
175
  def split_dotted(dotted)
132
176
  first, second = dotted.to_s.split(".", 2)
133
177
  [first.to_s, second.to_s]
134
178
  end
135
179
 
180
+ # The part of a dotted name before its first dot.
181
+ #
182
+ # @param dotted [String, Symbol, #to_s] text, optionally containing a dot
183
+ # @return [String, nil] the text before the first dot, or nil if `dotted`
184
+ # has no dot
136
185
  def qualifier(dotted)
137
186
  text = dotted.to_s
138
187
  text.include?(".") ? text.split(".", 2).first : nil
139
188
  end
140
189
 
190
+ # The part of a dotted name after its first dot.
191
+ #
192
+ # @param dotted [String, Symbol, #to_s] text, optionally containing a dot
193
+ # @return [String] the text after the first dot, or the whole text if
194
+ # `dotted` has no dot
141
195
  def unqualified(dotted)
142
196
  text = dotted.to_s
143
197
  text.include?(".") ? text.split(".", 2).last : text
144
198
  end
145
199
 
200
+ # Splits a domain-qualified verb into its domain, aggregate, and command parts.
201
+ #
146
202
  # Domain, aggregate, then the REST dot-joined into one command path.
147
203
  #
148
204
  # The `::` boundary between domain and aggregate is unambiguous by
@@ -166,6 +222,11 @@ module Hecks
166
222
  # .kind_for` and `ReactionInvocation#resolve_target` both already
167
223
  # assume this contract on their own end; this is what actually
168
224
  # delivers it to them.
225
+ #
226
+ # @param verb [String, Symbol, #to_s] a `Domain::Aggregate.command` (or
227
+ # `.query`) path
228
+ # @return [Array(String, String, String), nil] `[domain, aggregate, command]`,
229
+ # or nil if `verb` has no `.` or no `domain::aggregate` before it
169
230
  def split_verb(verb)
170
231
  path, command = verb.to_s.split(".", 2)
171
232
  return nil unless path && command
@@ -178,6 +239,9 @@ module Hecks
178
239
  [domain, aggregate, command]
179
240
  end
180
241
 
242
+ # Rewrites a bare command constant's trailing `::` into `.`, matching a
243
+ # command's own FQN separator; a String or Symbol passes through unchanged.
244
+ #
181
245
  # `trigger Account::Debit` / `dispatch Account::Debit` — a bare
182
246
  # constant reference (`ConstShim`'s own `ScopedConstant`, S0b), not
183
247
  # text (ADR 0025, "events and reactions" — command references become
@@ -195,6 +259,10 @@ module Hecks
195
259
  # it (its own last `::` sits between the domain and the aggregate,
196
260
  # not the aggregate and the command). Only an actual constant object
197
261
  # — never seen holding a `.` of its own — needs the rewrite at all.
262
+ #
263
+ # @param value [Symbol, String, Module] the command, as a bare constant (a
264
+ # `ScopedConstant` module `ConstShim` resolves) or already-dotted text
265
+ # @return [String] the dotted command reference
198
266
  def command_ref(value)
199
267
  return value.to_s if value.is_a?(::String) || value.is_a?(::Symbol)
200
268
 
@@ -203,6 +271,9 @@ module Hecks
203
271
  path.empty? ? text : "#{path}.#{command}"
204
272
  end
205
273
 
274
+ # The event-side twin of `command_ref`, kept as its own name because an
275
+ # event name is not a command name that happens to share a format.
276
+ #
206
277
  # `emits Account::AccountFrozen` / `on Account::AccountFrozen` — the
207
278
  # event-side twin of `command_ref`, above (ADR 0025, S6 — "events
208
279
  # first-class"). Identical transform (a bare `ScopedConstant`'s last
@@ -212,8 +283,14 @@ module Hecks
212
283
  # own name because the two references mean different things even
213
284
  # though the rewrite is byte-identical: an event name is not a
214
285
  # command name that happens to share a format.
286
+ #
287
+ # @param value [Symbol, String, Module] the event, as a bare constant (a
288
+ # `ScopedConstant` module `ConstShim` resolves) or already-dotted text
289
+ # @return [String] the dotted event reference
215
290
  def event_ref(value) = command_ref(value)
216
291
 
292
+ # Strips a process manager's event reference down to its bare event name.
293
+ #
217
294
  # `transition Account::AccountDebited => "state"` / `starts_on
218
295
  # Transfer::TransferRequested` / `ends_on Transfer::TransferSettled`
219
296
  # — a process manager's own event references (ADR 0025, S6),
@@ -240,6 +317,10 @@ module Hecks
240
317
  # passes through unchanged either way, exactly like `command_ref`'s
241
318
  # own legacy branch — this corpus never spelled one dotted to begin
242
319
  # with, so there is nothing here to strip.
320
+ #
321
+ # @param value [Symbol, String, Module] the event, as a bare constant (a
322
+ # `ScopedConstant` module `ConstShim` resolves) or already-dotted text
323
+ # @return [String] the bare event name, with any qualifier stripped
243
324
  def event_name_ref(value) = demodulise(value)
244
325
  end
245
326
  end
@@ -28,8 +28,7 @@ module Hecks
28
28
  # No adapter or spec double for this port ships in this repository, so every shape
29
29
  # below other than `registry` is adapter-defined: the port forwards it untouched.
30
30
  #
31
- # @param registry [Runtime::Registry] the booted registry, used to resolve the adapter
32
- # and handed on to it
31
+ # @param registry [Runtime::Registry] the booted registry to resolve the adapter against
33
32
  # @param identity_id [Object] adapter-defined identity key, forwarded unchanged
34
33
  # @return [Object] adapter-defined session representation
35
34
  # @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
@@ -44,8 +43,7 @@ module Hecks
44
43
  # answers `issuer`, `subject` and `email`), but nothing in this repository wires the
45
44
  # two together, so their shapes here are adapter-defined.
46
45
  #
47
- # @param registry [Runtime::Registry] the booted registry, used to resolve the adapter
48
- # and handed on to it
46
+ # @param registry [Runtime::Registry] the booted registry to resolve the adapter against
49
47
  # @param email [Object] adapter-defined, forwarded unchanged; the person's email
50
48
  # @param issuer [Object] adapter-defined, forwarded unchanged; the OIDC issuer that
51
49
  # authenticated the person
@@ -60,8 +58,7 @@ module Hecks
60
58
 
61
59
  # Asks the domain's adapter which roles it can grant.
62
60
  #
63
- # @param registry [Runtime::Registry] the booted registry, used to resolve the adapter
64
- # and handed on to it
61
+ # @param registry [Runtime::Registry] the booted registry to resolve the adapter against
65
62
  # @return [Object] adapter-defined collection of grantable roles
66
63
  # @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
67
64
  # (see `adapter`)
@@ -71,8 +68,7 @@ module Hecks
71
68
 
72
69
  # Grants a role to a person, by whatever means the domain's adapter records a grant.
73
70
  #
74
- # @param registry [Runtime::Registry] the booted registry, used to resolve the adapter
75
- # and handed on to it
71
+ # @param registry [Runtime::Registry] the booted registry to resolve the adapter against
76
72
  # @param email [Object] adapter-defined, forwarded unchanged; the person receiving the role
77
73
  # @param role [Object] adapter-defined, forwarded unchanged; the role to grant
78
74
  # @return [Object] adapter-defined representation of the grant
@@ -84,8 +80,7 @@ module Hecks
84
80
 
85
81
  # Lists every person the domain's adapter knows about.
86
82
  #
87
- # @param registry [Runtime::Registry] the booted registry, used to resolve the adapter
88
- # and handed on to it
83
+ # @param registry [Runtime::Registry] the booted registry to resolve the adapter against
89
84
  # @return [Object] adapter-defined collection of people
90
85
  # @raise [Runtime::WiringError] if this port does not resolve to exactly one adapter
91
86
  # (see `adapter`)
@@ -31,8 +31,7 @@ module Hecks
31
31
 
32
32
  # Answers whether an actor holds a live (not ended) grant of a role.
33
33
  #
34
- # @param registry [Runtime::Registry] the booted registry, used to resolve the adapter
35
- # and handed on to it
34
+ # @param registry [Runtime::Registry] the booted registry to resolve the adapter against
36
35
  # @param actor_id [String] the actor whose grants are checked
37
36
  # @param role [String, Symbol] the role name to look for, compared as a String
38
37
  # @param as_of [Integer, nil] Unix epoch seconds (from `Ports::Clock.now`); a grant whose
@@ -51,8 +50,7 @@ module Hecks
51
50
 
52
51
  # Answers whether one role may act as another.
53
52
  #
54
- # @param registry [Runtime::Registry] the booted registry, used to resolve the adapter
55
- # and handed on to it
53
+ # @param registry [Runtime::Registry] the booted registry to resolve the adapter against
56
54
  # @param from_role [String, Symbol] the role the caller holds, compared as a String
57
55
  # @param to_role [String, Symbol] the role the caller wants to act as, compared as a
58
56
  # String
@@ -67,8 +65,7 @@ module Hecks
67
65
 
68
66
  # Looks up the role an actor holds right now, rather than checking a guessed one.
69
67
  #
70
- # @param registry [Runtime::Registry] the booted registry, used to resolve the adapter
71
- # and handed on to it
68
+ # @param registry [Runtime::Registry] the booted registry to resolve the adapter against
72
69
  # @param actor_id [String] the actor to look up
73
70
  # @return [String, nil] the role name of the actor's first live (not ended) grant, or
74
71
  # nil if it has none; the caller supplies any fallback
@@ -27,8 +27,7 @@ module Hecks
27
27
  # No adapter, spec double or caller of this port ships in this repository, so every
28
28
  # shape below other than `registry` is adapter-defined: the port forwards it untouched.
29
29
  #
30
- # @param registry [Runtime::Registry] the booted registry, used to resolve the adapter
31
- # and handed on to it
30
+ # @param registry [Runtime::Registry] the booted registry to resolve the adapter against
32
31
  # @param agg_name [Object] adapter-defined, forwarded unchanged; names the aggregate
33
32
  # whose identity field is being assigned
34
33
  # @param field_name [Object] adapter-defined, forwarded unchanged; names the identity
@@ -19,8 +19,7 @@ module Hecks
19
19
 
20
20
  # Looks up the id of the identity an authenticated (issuer, subject) pair is linked to.
21
21
  #
22
- # @param registry [Runtime::Registry] the booted registry, used to resolve the adapter
23
- # and handed on to it
22
+ # @param registry [Runtime::Registry] the booted registry to resolve the adapter against
24
23
  # @param issuer [String] the OIDC issuer that authenticated the caller
25
24
  # @param subject [String] the OIDC subject the issuer vouches for
26
25
  # @return [String, nil] the linked identity's id, usable as an `actor_id` for
@@ -0,0 +1,6 @@
1
+ Hecks.port "key_vault" do
2
+ verb "vaulted_by"
3
+ signal :reply
4
+ answers :issue
5
+ answers :destroy
6
+ end
@@ -0,0 +1,58 @@
1
+ require_relative "../runtime/registry"
2
+
3
+ module Hecks
4
+ module Ports
5
+ # A subject's own encryption key lives here, one boundary removed from
6
+ # any event or bluebook attribute — cryptoshredding depends on the key
7
+ # material never being reachable except through this port's opaque
8
+ # reference (see `Privacy::SubjectKey`, `lib/hecks/framework/bluebook/
9
+ # privacy.bluebook`). Resolved the same way `Ports::IdentityGeneration`
10
+ # resolves its own adapter: one adapter registry-wide answers this
11
+ # port, not a per-aggregate binding.
12
+ module KeyVault
13
+ NAME = "key_vault".freeze
14
+
15
+ module_function
16
+
17
+ # Mints a fresh per-subject encryption key and hands back an opaque reference to it.
18
+ #
19
+ # @param registry [Runtime::Registry] the booted registry to resolve the adapter against
20
+ # @param subject_id [String] the data subject the key is being issued for
21
+ # @return [String] an opaque key reference; the key material itself never leaves the
22
+ # bound adapter, and is never suitable to store on an event or a bluebook attribute
23
+ def issue(registry, subject_id:) = adapter(registry).issue(subject_id: subject_id)
24
+
25
+ # Irrevocably destroys a key that {#issue} already returned a reference for.
26
+ #
27
+ # Ciphertext produced under `key_reference` becomes permanently unrecoverable the
28
+ # moment this returns — the mechanism a right-to-erasure request satisfies without
29
+ # rewriting or deleting any event.
30
+ #
31
+ # @param registry [Runtime::Registry] the booted registry to resolve the adapter against
32
+ # @param key_reference [String] the opaque reference {#issue} returned
33
+ # @return [Boolean] true when a live key was destroyed; false when this reference was
34
+ # already destroyed, or was never issued
35
+ def destroy(registry, key_reference:) = adapter(registry).destroy(key_reference: key_reference)
36
+
37
+ # Finds the single adapter bound to this port, refusing an ambiguous wiring.
38
+ #
39
+ # @param registry [Runtime::Registry] the booted registry to search
40
+ # @return [Module] the adapter module or class implementing this port
41
+ # @raise [Runtime::WiringError] if no adapter, or more than one, implements this port
42
+ def adapter(registry)
43
+ implementations = registry.adapters.values.select { |a| a.port == NAME }
44
+
45
+ case implementations.size
46
+ when 1 then registry.adapter_class(implementations.first.name)
47
+ when 0
48
+ raise Runtime::WiringError,
49
+ "no adapter implements the #{NAME} port — nothing can issue or destroy a key"
50
+ else
51
+ raise Runtime::WiringError,
52
+ "#{implementations.size} adapters implement the #{NAME} port " \
53
+ "(#{implementations.map(&:name).sort.join(', ')}) — the runtime will not choose for you"
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -196,6 +196,12 @@ module Hecks
196
196
  # duplicate manually after a merge; teaching this intersect
197
197
  # about a rekey mapping is real, separate work, deliberately
198
198
  # out of scope for rekey's first pass.
199
+ # @param aggregate [Bluebook::Aggregate] the aggregate to check for conflicting ids
200
+ # @param edges [Hash] the fork's own era edges, as `names_by_era` reads them
201
+ # @param era [Integer] the new world's own era
202
+ # @param cut [Integer] the ordinal the fork happened at
203
+ # @return [Array<Array(String, String)>] `[storage_name, aggregate_id]` pairs touched
204
+ # by both worlds since the cut
199
205
  def conflict_ids(aggregate, edges, era, cut)
200
206
  names = names_by_era(aggregate, edges)
201
207
  olds = (1...era).map { |ancestor| text_literal(names[:storage][ancestor - 1]) }.join(", ")
@@ -139,10 +139,28 @@ module Hecks
139
139
  "human approved no longer cover the data; re-run bin/translation_audit with --approve"
140
140
  end
141
141
 
142
+ # Refuses a mint that found no translation edge, naming the authoring tools —
143
+ # or, with scaffolding on, the file it just wrote.
144
+ #
142
145
  # No edge yet: the boot refuses toward the authoring loop —
143
146
  # naming both tools and the era ordinal. With HECKS_SCAFFOLD=1
144
147
  # the boot runs the scaffold first (an explicit flag, never a
145
148
  # silent side-effect) and the refusal names the file it wrote.
149
+ #
150
+ # @param registry [Runtime::Registry] the registry, forwarded to `scaffold!` when
151
+ # scaffolding is on
152
+ # @param bluebook [Bluebook::Chapter] the domain, named in the refusal
153
+ # @param lineage [Adapters::PostgresEra::Lineage] the domain's lineage, forwarded
154
+ # to `scaffold!` when scaffolding is on
155
+ # @param latest [Hash{Symbol => Object}] the newest held era, forwarded to
156
+ # `scaffold!` when scaffolding is on
157
+ # @param ordinal [Integer] the ordinal of the era about to be minted, named in
158
+ # the refusal
159
+ # @param directory [String, nil] the domain's bluebook directory; scaffolding
160
+ # requires this to be present as well as `HECKS_SCAFFOLD=1`
161
+ # @return [void] never returns
162
+ # @raise [Runtime::WiringError] always; the message names the scaffolded file
163
+ # when `HECKS_SCAFFOLD=1` and `directory` wrote one
146
164
  def refuse_toward_the_scaffold!(registry, bluebook, lineage, latest, ordinal, directory)
147
165
  if ENV["HECKS_SCAFFOLD"] == "1" && directory
148
166
  path = scaffold!(registry, bluebook, lineage, latest, directory)
@@ -157,9 +175,19 @@ module Hecks
157
175
  "check it with bin/translation_audit, then boot again"
158
176
  end
159
177
 
160
- # Diff the held era against the current shape and write the edge
178
+ # Diffs the held era against the current shape and writes the edge
161
179
  # file — confident rules inline, ambiguities as parse-refusing
162
- # `unresolved` lines. Returns the file path.
180
+ # `unresolved` lines.
181
+ #
182
+ # @param _registry [Runtime::Registry] unused; kept so the signature matches its
183
+ # caller's own arguments
184
+ # @param bluebook [Bluebook::Chapter] the domain as currently declared
185
+ # @param lineage [Adapters::PostgresEra::Lineage] the domain's lineage; named so
186
+ # `ensure_named!` can mint a name for `latest` first if it has none
187
+ # @param latest [Hash{Symbol => Object}] the newest held era, as `Lineage#eras`
188
+ # returns it
189
+ # @param directory [String] the domain's bluebook directory to write the edge under
190
+ # @return [String] the path `Translation::Scaffold::Writer#write!` wrote
163
191
  def scaffold!(_registry, bluebook, lineage, latest, directory)
164
192
  ensure_named!(lineage, latest)
165
193
  latest = lineage.eras.last
@@ -179,9 +207,17 @@ module Hecks
179
207
  Translation::Scaffold.write!(directory, edge)
180
208
  end
181
209
 
210
+ # Mints a name for an era that has none yet, leaving an already-named era untouched.
211
+ #
182
212
  # Era names are minted once. An era held before any drift was
183
213
  # seen has no name yet; it gets one the moment an edge needs to
184
214
  # leave it.
215
+ #
216
+ # @param lineage [Adapters::PostgresEra::Lineage] the domain's lineage, which
217
+ # records the minted name
218
+ # @param era [Hash{Symbol => Object}] the era to name, as `Lineage#eras` returns it;
219
+ # `:hash` present means it is already named
220
+ # @return [void]
185
221
  def ensure_named!(lineage, era)
186
222
  return if era[:hash]
187
223
 
@@ -92,6 +92,12 @@ module Hecks
92
92
  # header names. Only reached once `layer_two!` has already ruled
93
93
  # out "no declared edge" and "rekeyed" (see its own comment on
94
94
  # that second guard); this method assumes both are false.
95
+ # @param violations [Array<String>] mutated in place with one message per divergent id
96
+ # @param aggregate [Bluebook::Aggregate] the aggregate being audited
97
+ # @param declared [Bluebook::Translation] the declared translation to check against
98
+ # @param before [Hash{String => Hash}] the old world's own records, keyed by id
99
+ # @param after [Hash{String => Hash}] the new world's own records, keyed by id
100
+ # @return [void]
95
101
  def check_value_preservation!(violations, aggregate, declared, before, after)
96
102
  rules = Ports::Persistence::Lineage.from_declared(declared, aggregate.name)
97
103
  compute_paths = declared.computes.flat_map { |compute| [compute.from, compute.to] }.map(&:to_s)
@@ -26,6 +26,13 @@ module Hecks
26
26
  # in the reference transform's phase order (renames, moves,
27
27
  # converts, drops), computes last. `retype` compiles to nothing:
28
28
  # stored state never carries a type name.
29
+ # Compiles a declared edge's rename/move/convert/drop/compute rules into one nested SQL
30
+ # expression over the `state` jsonb column.
31
+ #
32
+ # @param declared [Bluebook::TranslationAggregate] this edge's declared rules for one
33
+ # aggregate
34
+ # @return [String] a SQL expression, `"state"` unchanged when `declared` declares none
35
+ # of the five rule kinds
29
36
  def compile_rules(declared)
30
37
  expression = "state"
31
38
  declared.renames.each do |old_name, new_name|
@@ -57,6 +64,11 @@ module Hecks
57
64
  # `Ports::Persistence::Lineage` wrapper the app-level consumers
58
65
  # (coverage_check.rb, minter.rb, layer_two.rb) go through — this
59
66
  # module builds SQL straight off the IR either way.
67
+ # Reports whether an edge's declared rules for an aggregate include a rekey.
68
+ #
69
+ # @param declared [Bluebook::TranslationAggregate, nil] this edge's declared rules for
70
+ # one aggregate; nil for an aggregate the edge declares nothing about
71
+ # @return [Boolean] true when `declared` is present and its `rekeys` is non-empty
60
72
  def rekeyed?(declared) = declared && !declared.rekeys.empty?
61
73
 
62
74
  # The only two places `aggregate_id` needs to change — guarded so
@@ -64,6 +76,12 @@ module Hecks
64
76
  # declared) stays the bare `aggregate_id` passthrough it always
65
77
  # was — this case only appears in an edge that actually declares
66
78
  # one.
79
+ #
80
+ # @param guard [String] a SQL boolean expression gating when the rekey applies, such as
81
+ # `"operation = 'save'"`
82
+ # @param declared [Bluebook::TranslationAggregate] this edge's declared rules; must
83
+ # declare a rekey (`rekeyed?(declared)` true)
84
+ # @return [String] a `"CASE WHEN ... END AS aggregate_id"` SQL expression
67
85
  def id_case(guard, declared)
68
86
  "CASE WHEN #{guard} THEN #{compile_id_expression(declared)} ELSE aggregate_id END AS aggregate_id"
69
87
  end
@@ -75,6 +93,11 @@ module Hecks
75
93
  # it needs to see first — it reads the record's stored fields
76
94
  # exactly as they already are, the same `__s` convention
77
95
  # `compile_compute` exposes.
96
+ #
97
+ # @param declared [Bluebook::TranslationAggregate] this edge's declared rules; its
98
+ # first `rekeys` entry supplies the SQL
99
+ # @return [String] a SQL expression evaluating the rekey's own SQL against the record's
100
+ # current `state`
78
101
  def compile_id_expression(declared)
79
102
  rekey = declared.rekeys.first
80
103
  "(SELECT (#{rekey.sql}) FROM (SELECT (state) AS __s) __outer)"
@@ -84,6 +107,12 @@ module Hecks
84
107
  # — evaluated exclusively inside the compiled head, never
85
108
  # in-process. The old field is exposed under its own name (as
86
109
  # text, exactly as the author's expression expects to cast it).
110
+ #
111
+ # @param expression [String] the SQL expression built so far by `compile_rules`, read as
112
+ # `__s` inside the compute's own SQL
113
+ # @param compute [Bluebook::TranslationCompute] the declared compute rule
114
+ # @return [String] `expression` wrapped so the compute's field lands at its declared
115
+ # destination when the source field is present, unchanged otherwise
87
116
  def compile_compute(expression, compute)
88
117
  from = compute.from.to_s
89
118
  to = compute.to.to_s
@@ -103,13 +132,24 @@ module Hecks
103
132
  # non-Postgres-bound domain (there are none today, but nothing
104
133
  # here should assume there never will be) doesn't gain a hard
105
134
  # dependency on `pg` just by loading this file.
135
+ #
136
+ # @param name [String, Symbol] the identifier to quote
137
+ # @return [String] `name` as a double-quoted Postgres identifier
106
138
  def quote(name)
107
139
  require "pg"
108
140
  PG::Connection.quote_ident(name.to_s)
109
141
  end
110
142
 
143
+ # Renders a Ruby value as a single-quoted SQL text literal, escaping embedded quotes.
144
+ #
145
+ # @param text [String, Symbol, Object] the value to render; converted with `to_s`
146
+ # @return [String] a single-quoted SQL literal
111
147
  def text_literal(text) = "'#{text.to_s.gsub("'", "''")}'"
112
148
 
149
+ # Renders a dotted path as a SQL `text[]` array literal, one element per segment.
150
+ #
151
+ # @param path [String, Symbol] a bare or dotted path, such as `"price.cents"`
152
+ # @return [String] a SQL `ARRAY[...]::text[]` expression
113
153
  def path_literal(path)
114
154
  segments = path.to_s.split(".").map { |segment| text_literal(segment) }
115
155
  "ARRAY[#{segments.join(', ')}]::text[]"