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
@@ -22,10 +22,26 @@ module Hecks
22
22
  class QueryInterpreter
23
23
  attr_reader :registry
24
24
 
25
+ # @param registry [Runtime::Registry] the booted registry queries are answered
26
+ # against
25
27
  def initialize(registry)
26
28
  @registry = registry
27
29
  end
28
30
 
31
+ # Answers one declared aggregate or entity query, preferring a native adapter
32
+ # hook and falling back to interpreting the query over every loaded record.
33
+ #
34
+ # @param domain [String, Symbol] the domain `aggregate` belongs to
35
+ # @param aggregate [Bluebook::Aggregate] the aggregate the query is declared on
36
+ # @param query_name [String] the query's declared name, or an entity query's
37
+ # dotted `"Entity.Query"` name
38
+ # @param args [Hash{Symbol => Object}] the query's declared arguments
39
+ # @return [Array<Hash>] one frozen Hash per matching record, its state with
40
+ # `:id` merged in last
41
+ # @raise [Runtime::UnknownVerb] if `query_name` names no declared query
42
+ # @raise [Runtime::TypeMismatch] if an argument cannot be coerced to its
43
+ # declared type
44
+ # @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
29
45
  def call(domain, aggregate, query_name, args)
30
46
  return entity_rows(domain, aggregate, query_name, args) if query_name.include?(".")
31
47
 
@@ -55,7 +71,7 @@ module Hecks
55
71
  # Instance#to_h's own comment: an aggregate free to declare its
56
72
  # own attribute literally named `id` has that attribute's own
57
73
  # wrapped value sitting in `record.state[:id]` already; merging
58
- # it over a `{id:}.merge(state)` used to let it silently
74
+ # it over a `{id:}.merge(state)` would let it silently
59
75
  # clobber the correct bare identity this row is supposed to
60
76
  # carry.
61
77
  # A query row is an answer, not a handle. Mutating one edits
@@ -81,6 +97,18 @@ module Hecks
81
97
  # adapters, but the fold itself — the empty candidate set, a
82
98
  # duplicate id, a dangling reference, a chain's inside-out
83
99
  # resolution order — would only ever be compared against itself.
100
+ #
101
+ # @param domain [String, Symbol] the domain `aggregate` belongs to
102
+ # @param aggregate [Bluebook::Aggregate] the aggregate the query is declared on
103
+ # @param query_name [String] the query's declared name, or an entity query's
104
+ # dotted `"Entity.Query"` name
105
+ # @param args [Hash{Symbol => Object}] the query's declared arguments
106
+ # @return [Array<Hash>] one Hash per matching record, its state with `:id`
107
+ # merged in last
108
+ # @raise [Runtime::UnknownVerb] if `query_name` names no declared query
109
+ # @raise [Runtime::TypeMismatch] if an argument cannot be coerced to its
110
+ # declared type
111
+ # @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
84
112
  def reference_call(domain, aggregate, query_name, args)
85
113
  return entity_rows(domain, aggregate, query_name, args) if query_name.include?(".")
86
114
 
@@ -104,9 +132,9 @@ module Hecks
104
132
  ordered = ordered(matched, declared.order_by, declared.null_semantics)
105
133
  # **Offset first, then limit** — the order SQL means by `LIMIT n
106
134
  # OFFSET m`, and the order Ports::Query::InMemory#execute already
107
- # applies (see that file's own comment). This interpreter used to
108
- # never read declared.offset at all — offset silently vanished for
109
- # any query answered here, not just come out reversed.
135
+ # applies (see that file's own comment). Without reading
136
+ # `declared.offset`, offset would silently vanish for any query
137
+ # answered here, not just come out reversed.
110
138
  skipped = declared.offset ? ordered.drop(resolve_query_value(declared.offset.value, args).to_i) : ordered
111
139
  capped = declared.limit ? skipped.first(resolve_query_value(declared.limit.value, args).to_i) : skipped
112
140
 
@@ -249,12 +277,12 @@ module Hecks
249
277
  end
250
278
 
251
279
  # The comparator table itself lives in
252
- # QuerySpecification::Common::Comparison. This method and
253
- # Ports::Query::InMemory#holds? used to carry a copy each and the
254
- # two drifted — `none_in_state` reached only one of them, and
255
- # `comparable` disagreed about value objects with two numeric
256
- # members. What stays here is how a value is reached for this
257
- # path: the registry is instance state rather than an argument.
280
+ # QuerySpecification::Common::Comparison, shared with
281
+ # Ports::Query::InMemory#holds? rather than each carrying its own
282
+ # copy — two copies once drifted: `none_in_state` reached only one
283
+ # of them, and `comparable` disagreed about value objects with two
284
+ # numeric members. What stays here is how a value is reached for
285
+ # this path: the registry is instance state rather than an argument.
258
286
  def holds?(clause, held, args, record: nil, domain: nil)
259
287
  QuerySpecification::Common::Comparison.holds?(
260
288
  clause.op, comparable(held), comparable(resolve_query_value(clause.value, args)), registry: @registry
@@ -273,8 +301,8 @@ module Hecks
273
301
  # the same way a command argument's own is, so a `nil` offered for
274
302
  # a non-optional value-object-typed query attribute
275
303
  # (Governance::RoleAssignment.AssignmentsForActor's `actor_id`, say)
276
- # has to refuse — passing it through unchecked (as this used to)
277
- # let it through as a silent, unfiltered query instead, a real
304
+ # has to refuse — passing it through unchecked would let it through
305
+ # as a silent, unfiltered query instead, a real
278
306
  # Ruby/Rust divergence the fuzzer caught (QualityControl BUG#2).
279
307
  #
280
308
  # `checked_vo?` true is handled by `null_vo_argument!` directly,
@@ -6,10 +6,10 @@ require_relative "value"
6
6
  module Hecks
7
7
  module Runtime
8
8
  # Turns facts selected by a policy or process manager into the same
9
- # receiver/payload envelope an outside caller uses. Reaction declarations
10
- # historically selected both through one `with:` map, so this is the one
11
- # compatibility seam that separates receiver identities from facts after
12
- # resolving the declaration and before re-entering the dispatcher.
9
+ # receiver/payload envelope an outside caller uses. A reaction declares
10
+ # both receiver identities and facts through one `with:` map, so this is
11
+ # the one compatibility seam that separates them after resolving the
12
+ # declaration and before re-entering the dispatcher.
13
13
  module ReactionInvocation
14
14
  Target = Struct.new(:aggregate, :entities, :command, keyword_init: true)
15
15
  # :facts, not :values — Struct.new already defines #values (every
@@ -19,10 +19,17 @@ module Hecks
19
19
 
20
20
  module_function
21
21
 
22
- # The holding IR historically represented both an omitted projection and
23
- # an explicitly empty `with: {}` as the same empty array. Builders now
24
- # preserve declaration presence off-wire; reconstructed/legacy IR falls
25
- # back to the old non-empty reading.
22
+ # Reports whether a reaction declared an explicit `with:` projection.
23
+ #
24
+ # An omitted projection and an explicitly empty `with: {}` both hold
25
+ # as the same empty array on the holding IR. Builders preserve
26
+ # declaration presence off-wire, in `@projection_declared`;
27
+ # reconstructed/legacy IR without that ivar falls back to reading
28
+ # presence off whether `with_spec` is non-empty.
29
+ #
30
+ # @param declaration [Bluebook::Policy, Bluebook::DispatchSpec] the reacting
31
+ # declaration to check
32
+ # @return [Boolean] true if the declaration names an explicit `with:` projection
26
33
  def projection_declared?(declaration)
27
34
  if declaration.instance_variable_defined?(:@projection_declared)
28
35
  declaration.instance_variable_get(:@projection_declared)
@@ -31,11 +38,27 @@ module Hecks
31
38
  end
32
39
  end
33
40
 
41
+ # Resolves a declared `with:` projection against the scopes and bindings visible to it.
42
+ #
34
43
  # A reaction's source names resolve lexically, not globally. Policies
35
44
  # supply one event/row scope. Process managers supply current event then
36
45
  # opening-event memory, while correlation is an explicit binding ahead
37
46
  # of both. Missing names are refused here rather than materialized as nil
38
47
  # and accidentally presented as target command facts.
48
+ #
49
+ # @param with_spec [Hash{Symbol => Object}] each target fact name mapped to its source:
50
+ # a Symbol naming a fact visible in `bindings` or `scopes`, or any other value taken
51
+ # as a literal
52
+ # @param scopes [Array<Hecks::Runtime::ReactionInvocation::Scope, Array(String,
53
+ # Hash)>] the named fact scopes to resolve a Symbol source against, checked in order;
54
+ # a bare `[name, facts]` pair is wrapped into a `Scope`
55
+ # @param bindings [Hash] explicit bindings (such as a saga's correlation key), checked
56
+ # before any scope
57
+ # @param label [String] names this resolution in an `UnknownArgument` refusal
58
+ # @return [Hash{Symbol => Object}] `with_spec`'s keys mapped to their resolved,
59
+ # materialized values
60
+ # @raise [Runtime::UnknownArgument] if a Symbol source names a fact visible in no
61
+ # binding and no scope
39
62
  def resolve_mapping(with_spec:, scopes:, bindings: {}, label: "reaction")
40
63
  normalized_bindings = bindings.transform_keys(&:to_sym)
41
64
  normalized_scopes = scopes.map do |scope|
@@ -86,6 +109,28 @@ module Hecks
86
109
  # piece that is self-contained; what remains is the sequencing
87
110
  # itself, which further splitting would only relocate, not remove.
88
111
  # rubocop:disable-next Metrics/MethodLength, Metrics/PerceivedComplexity
112
+ #
113
+ # @param registry [Runtime::Registry] the booted registry to resolve `verb` against
114
+ # @param verb [String] the fully qualified target command verb
115
+ # @param projected [Hash] the facts to send, already resolved (e.g. by
116
+ # `resolve_mapping`) or, for a legacy reaction, the raw event/row payload
117
+ # @param explicit [Boolean] true when the reaction declared its own `with:` projection
118
+ # (`projection_declared?`); false forwards `projected` wholesale as legacy args
119
+ # @param passthrough [Array<String, Symbol>] extra fact names allowed to ride along
120
+ # unconsumed, beyond the receiver identity and declared command facts
121
+ # @param source_receiver [Hash{Symbol => Object}, nil] the triggering event's own
122
+ # `{aggregate:, identity:}`, offered as a same-aggregate receiver when nothing else
123
+ # supplies one; nil when there is no such event to inherit from
124
+ # @return [Hash{Symbol => Object}] `{to:, with:}` for an explicit projection targeting
125
+ # a non-creating command (`with:` only for a creating command); otherwise `projected`
126
+ # (with `to:` merged in when a receiver could be inherited)
127
+ # @raise [Runtime::UnknownVerb] if `verb` does not resolve to a declared command,
128
+ # entity command, or port operation (only when `explicit` is true; a legacy call
129
+ # resolving `verb` only to check inheritance swallows this and forwards unchanged)
130
+ # @raise [Runtime::TypeMismatch] if an explicit projection resolves no receiver
131
+ # identity for the target aggregate or one of its entities
132
+ # @raise [Runtime::UnknownArgument] if an explicit projection's facts include a name
133
+ # that is neither a consumed receiver identity nor a declared command fact
89
134
  def build(registry:, verb:, projected:, explicit:, passthrough: [], source_receiver: nil)
90
135
  args = projected.transform_keys(&:to_sym)
91
136
  unless explicit
@@ -16,16 +16,32 @@ module Hecks
16
16
  # model is simple enough for one; otherwise runs the whole join
17
17
  # in-process against loaded records.
18
18
  class ReadModelInterpreter
19
+ # @param registry [Runtime::Registry] the booted registry whose repositories
20
+ # this interpreter reads
19
21
  def initialize(registry) = @registry = registry
20
22
 
23
+ # Runs one declared read model and returns its projected rows.
24
+ #
25
+ # @param domain [String, Symbol] the domain the read model is declared in
26
+ # @param model [Bluebook::ReadModel] the read model to run
27
+ # @param args [Hash{Symbol => Object}] the query's declared arguments
28
+ # @return [Array<Hash>] a one-element Array holding a Hash of head name to
29
+ # projected rows (or a single row, for a non-`:many` head)
30
+ # @raise [Runtime::TypeMismatch] if the reference argument is offered as a whole
31
+ # object rather than a plain identity, or a `median` field is not numeric
32
+ # @raise [Runtime::NotFound] if the reference argument names no record
33
+ # @raise [KeyError] if a rooted read model is asked without its reference argument
34
+ # @raise [ArgumentError] if `group_by` or `median` names a field its target
35
+ # aggregate does not declare
36
+ # @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
21
37
  def call(domain, model, args)
22
38
  project(domain, model, args)
23
39
  end
24
40
 
25
41
  private
26
42
 
27
- # **Root-first, then the SQLite escape hatch, then the join loop** —
28
- # each step's own comment names a real, previously-shipped bug the
43
+ # Root-first, then the SQLite escape hatch, then the join loop —
44
+ # each step's own comment names a real, already-shipped bug the
29
45
  # current order fixes (the reference/TenantScope refusal ordering
30
46
  # above, the root-first head processing below). Splitting this
31
47
  # into smaller methods would scatter that ordering across method
@@ -77,11 +93,11 @@ module Hecks
77
93
  # bluebook. `read_model_builder.rb`'s own `include` is
78
94
  # documented "Order-independent" (the `:many` flag is resolved
79
95
  # at build time, once `@reference_target` is known), but that
80
- # promise was never kept here: this loop used to run heads in
81
- # their literal declared order and match each "many" head
82
- # against whatever was already in `projected` — empty, the
83
- # very first time through, if a many-side head happened to be
84
- # declared before the root. A real, live bug (not a guess):
96
+ # promise is not kept without this: running heads in
97
+ # their literal declared order and matching each "many" head
98
+ # against whatever was already in `projected` would leave it
99
+ # empty, the very first time through, if a many-side head happened
100
+ # to be declared before the root. A real, live bug (not a guess):
85
101
  # `include Promotion` before `include Item` on a read model
86
102
  # whose root is Item silently returned an empty array for
87
103
  # Promotion — no error, just a wrong, too-small answer — while
@@ -32,6 +32,12 @@ module Hecks
32
32
  # `save` only runs when a projected value would differ from what
33
33
  # is already stored, so re-running a sweep with nothing having
34
34
  # moved on the target side touches the append log not at all.
35
+ #
36
+ # @param registry [Runtime::Registry] the booted registry to read repositories from
37
+ # @param domain [String] the domain the aggregate belongs to
38
+ # @param aggregate [Bluebook::Aggregate] the aggregate whose `projected_fields` to
39
+ # refresh
40
+ # @return [Integer] the number of records actually changed and saved
35
41
  def call(registry, domain, aggregate)
36
42
  return 0 if aggregate.projected_fields.empty?
37
43
 
@@ -39,6 +45,16 @@ module Hecks
39
45
  repository.all.count { |record| refresh(registry, domain, aggregate, record, repository) }
40
46
  end
41
47
 
48
+ # Refreshes one record's own projected fields in place, saving it if any changed.
49
+ #
50
+ # @param registry [Runtime::Registry] the booted registry to read the target's
51
+ # repository from
52
+ # @param domain [String] the domain the aggregate belongs to
53
+ # @param aggregate [Bluebook::Aggregate] the aggregate `record` is an instance of
54
+ # @param record [Runtime::Instance] the record to refresh, mutated in place
55
+ # @param repository [Ports::Persistence::AppendOnly] the repository to save `record`
56
+ # through when it changes
57
+ # @return [Boolean] true if any projected field's value changed and `record` was saved
42
58
  def refresh(registry, domain, aggregate, record, repository)
43
59
  changed = false
44
60
 
@@ -55,10 +71,22 @@ module Hecks
55
71
  changed
56
72
  end
57
73
 
74
+ # Reads the current value of one projected field's remote target field.
75
+ #
58
76
  # `nil` when the reference itself does not resolve in this
59
77
  # chapter (a cross-domain target left "unfollowed" the same way
60
78
  # References#dereference already leaves one) or when the record
61
79
  # names no target at all — an optional reference nobody set.
80
+ #
81
+ # @param registry [Runtime::Registry] the booted registry to read the target's
82
+ # repository from
83
+ # @param domain [String] the domain the aggregate belongs to
84
+ # @param aggregate [Bluebook::Aggregate] the aggregate declaring `field`
85
+ # @param record [Runtime::Instance] the record holding the reference to follow
86
+ # @param field [Bluebook::ProjectedField] the projected field to resolve
87
+ # @return [Object, nil] the target record's own `field.remote_field` value; nil if the
88
+ # reference type does not resolve, the record names no target, or the target record
89
+ # cannot be found
62
90
  def remote_value(registry, domain, aggregate, record, field)
63
91
  target = aggregate.attribute(field.reference)&.type&.resolve
64
92
  return nil unless target
@@ -31,6 +31,21 @@ module Hecks
31
31
  module ReferenceHop
32
32
  module_function
33
33
 
34
+ # Folds every hop clause in `declared.wheres` into a synthetic local `in` clause.
35
+ #
36
+ # @param declared [Bluebook::Query, Runtime::TenantScope::Scoped,
37
+ # QuerySpecification::Common::Options] the declared query specification to fold hop
38
+ # clauses of
39
+ # @param args [Hash] the query's arguments, read when resolving each hop's own query
40
+ # @param registry [Runtime::Registry] the booted registry to resolve each hop's target
41
+ # repository from
42
+ # @param domain [String] the domain `aggregate` belongs to
43
+ # @param aggregate [Bluebook::Aggregate] the aggregate `declared` queries
44
+ # @return [Bluebook::Query, Runtime::TenantScope::Scoped, QuerySpecification::Common::
45
+ # Options, Hecks::Runtime::ReferenceHop::Folded] `declared` unchanged when it has no
46
+ # hop clauses; otherwise a `Folded` wrapper whose `#wheres` replaces each hop clause
47
+ # with its folded `in` clause
48
+ # @raise [Runtime::WiringError] if a hop's target no longer resolves (see `fold`)
34
49
  def apply(declared, args, registry:, domain:, aggregate:)
35
50
  hopped, local = declared.wheres.partition { |clause| QuerySpecification::HopPath.hop_head?(clause.field, aggregate.attributes) }
36
51
  return declared if hopped.empty?
@@ -39,6 +54,18 @@ module Hecks
39
54
  Folded.new(declared, local + folded)
40
55
  end
41
56
 
57
+ # Folds one hop clause into a synthetic `in` clause over the hop attribute's own ids.
58
+ #
59
+ # @param clause [QuerySpecification::Common::WhereClause] the hop clause to fold; its
60
+ # `field` names the hop path, dotted past the first segment
61
+ # @param args [Hash] the query's arguments, read when resolving the inner query
62
+ # @param registry [Runtime::Registry] the booted registry to resolve the hop's target
63
+ # repository from
64
+ # @param domain [String] the domain `aggregate` belongs to
65
+ # @param aggregate [Bluebook::Aggregate] the aggregate `clause` is declared against
66
+ # @return [QuerySpecification::Common::WhereClause] a synthetic `in` clause on the hop
67
+ # attribute's name, whose value is every id the inner clause admits on the target
68
+ # @raise [Runtime::WiringError] if the hop's target aggregate no longer resolves
42
69
  def fold(clause, args, registry:, domain:, aggregate:)
43
70
  step = QuerySpecification::HopPath.next_hop(clause.field, aggregate.attributes)
44
71
  hop, rest = step
@@ -70,6 +97,16 @@ module Hecks
70
97
  # aggregate is actually bound to (which may not be the engine
71
98
  # the outer aggregate is bound to at all) rather than by a
72
99
  # second reading of the comparators.
100
+ #
101
+ # @param domain [String] the domain `target` belongs to
102
+ # @param target [Bluebook::Aggregate] the hop's target aggregate to query
103
+ # @param wheres [Array<QuerySpecification::Common::WhereClause>] the inner clause(s) to
104
+ # run against `target`
105
+ # @param args [Hash] the outer query's arguments, read when resolving the inner query
106
+ # @param registry [Runtime::Registry] the booted registry to resolve `target`'s
107
+ # repository from
108
+ # @return [Array<String>] every distinct id the inner clause(s) admit on `target`
109
+ # @raise [Runtime::WiringError] if a hop nested inside `wheres` no longer resolves
73
110
  def matching_ids(domain, target, wheres, args, registry:)
74
111
  spec = apply(QuerySpecification::Common::Options.new(wheres: wheres), args,
75
112
  registry: registry, domain: domain, aggregate: target)
@@ -87,6 +124,11 @@ module Hecks
87
124
  # same reason: SimpleDelegator only intercepts calls made
88
125
  # directly on the wrapper.
89
126
  class Folded < SimpleDelegator
127
+ # @param declared [Bluebook::Query, Runtime::TenantScope::Scoped,
128
+ # QuerySpecification::Common::Options] the wrapped query specification, delegated to
129
+ # for everything but `#wheres`
130
+ # @param wheres [Array<QuerySpecification::Common::WhereClause>] the replacement
131
+ # where-clauses, hop clauses folded to synthetic `in` clauses
90
132
  def initialize(declared, wheres)
91
133
  super(declared)
92
134
  @wheres = wheres
@@ -31,9 +31,18 @@ module Hecks
31
31
 
32
32
  module_function
33
33
 
34
+ # Renders `refusal`/`site`'s template, substituting `values` verbatim.
35
+ #
34
36
  # Plain text substitution, never expression syntax — a template is
35
37
  # read, not evaluated. Values arrive already formatted; prefer
36
38
  # `render_site`, which formats them off the declared rows.
39
+ #
40
+ # @param refusal [String] the `DomainRefusal` class name, such as `"UnknownArgument"`
41
+ # @param site [String] the template site within `refusal`, such as `"unknown_args"`
42
+ # @param values [Hash{Symbol => #to_s}] each `{name}` placeholder's already-formatted
43
+ # replacement text
44
+ # @return [String] the rendered refusal message
45
+ # @raise [KeyError] if no `RefusalTemplate` row declares `refusal`/`site`
37
46
  def render(refusal, site, **values)
38
47
  substitute(template(refusal, site), values)
39
48
  end
@@ -46,6 +55,16 @@ module Hecks
46
55
  # RefusalWording.render_site("UnknownArgument", "unknown_args",
47
56
  # command: "Close", unknown: [:parcel], declared: [])
48
57
  # # => "Close does not declare parcel — it takes none"
58
+ #
59
+ # @param refusal [String] the `DomainRefusal` class name, such as `"UnknownArgument"`
60
+ # @param site [String] the template site within `refusal`, such as `"unknown_args"`
61
+ # @param arguments [Hash{Symbol => Object}] raw values, one per `RefusalSiteArgument`
62
+ # row declared for `refusal`/`site`; formatted per row before substitution
63
+ # @return [String] the rendered refusal message
64
+ # @raise [ArgumentError] if `arguments` is missing a declared argument or offers one
65
+ # `refusal`/`site` does not declare
66
+ # @raise [KeyError] if no `RefusalTemplate`/`RefusalSiteArgument` rows declare
67
+ # `refusal`/`site`
49
68
  def render_site(refusal, site, **arguments)
50
69
  specs = argument_rows(refusal, site)
51
70
  declared = specs.map { |spec| spec["argument"].to_sym }
@@ -62,6 +81,12 @@ module Hecks
62
81
  # `render_site` without the registry lookups: a template, its
63
82
  # argument rows, and raw values. The Rust projection calls this with
64
83
  # the chapter's own rows to compute the expected wording it pins.
84
+ #
85
+ # @param template [String] the raw template, `{name}` placeholders unsubstituted
86
+ # @param specs [Array<Hash>] the `RefusalSiteArgument` rows to format `arguments`
87
+ # against, one per declared argument name
88
+ # @param arguments [Hash{Symbol => Object}] raw values, one per entry in `specs`
89
+ # @return [String] the rendered refusal message
65
90
  def render_with(template, specs, arguments)
66
91
  values = specs.to_h do |spec|
67
92
  name = spec["argument"].to_sym
@@ -73,6 +98,12 @@ module Hecks
73
98
  # One argument, written the way its row says. A list is sorted first
74
99
  # (before quoting), then each item quoted, then joined; an empty list
75
100
  # reads `when_empty`. A scalar is quoted or taken as its own text.
101
+ #
102
+ # @param spec [Hash] the argument's `RefusalSiteArgument` row (`"shape"`, `"quoting"`,
103
+ # and, for a list, `"sorted"`, `"separator"`, `"when_empty"`)
104
+ # @param value [Object, Array] the raw value to format; an Array (or anything
105
+ # `Array()`-coercible) for a `"list"`-shaped spec, a scalar otherwise
106
+ # @return [String] the formatted text
76
107
  def format_argument(spec, value)
77
108
  inspect = spec.fetch("quoting") == "inspect"
78
109
  return inspect ? value.inspect : value.to_s unless spec.fetch("shape") == "list"
@@ -83,10 +114,22 @@ module Hecks
83
114
  items.empty? ? spec.fetch("when_empty") : items.join(spec.fetch("separator"))
84
115
  end
85
116
 
117
+ # Replaces each `{name}` placeholder in `template` with its value's text.
118
+ #
119
+ # @param template [String] the raw template, `{name}` placeholders unsubstituted
120
+ # @param values [Hash{Symbol => #to_s}] each placeholder name mapped to its
121
+ # replacement text
122
+ # @return [String] `template` with every `{name}` placeholder substituted
86
123
  def substitute(template, values)
87
124
  values.reduce(template) { |text, (key, value)| text.gsub("{#{key}}", value.to_s) }
88
125
  end
89
126
 
127
+ # Looks up the raw template text declared for one refusal site.
128
+ #
129
+ # @param refusal [String] the `DomainRefusal` class name, such as `"UnknownArgument"`
130
+ # @param site [String] the template site within `refusal`, such as `"unknown_args"`
131
+ # @return [String] the raw `RefusalTemplate` text, `{name}` placeholders unsubstituted
132
+ # @raise [KeyError] if no `RefusalTemplate` row declares `refusal`/`site`
90
133
  def template(refusal, site)
91
134
  TEMPLATES.fetch([refusal, site]) do
92
135
  raise KeyError, "no refusal template for #{refusal}/#{site} — declare it in " \
@@ -94,9 +137,16 @@ module Hecks
94
137
  end
95
138
  end
96
139
 
140
+ # The declared arguments for one refusal site.
141
+ #
97
142
  # Read lazily, not into a constant: bin/project_vocabulary boots
98
143
  # `hecks` (and so this file) before it writes the table a newly
99
144
  # declared site's rows live in.
145
+ #
146
+ # @param refusal [String] the `DomainRefusal` class name, such as `"UnknownArgument"`
147
+ # @param site [String] the template site within `refusal`, such as `"unknown_args"`
148
+ # @return [Array<Hash>] the `RefusalSiteArgument` rows declared for `refusal`/`site`
149
+ # @raise [KeyError] if no `RefusalSiteArgument` rows declare `refusal`/`site`
100
150
  def argument_rows(refusal, site)
101
151
  @argument_rows ||= Hecks::Vocabulary.rows("RefusalSiteArgument")
102
152
  .group_by { |row| [row["refusal"], row["site"]] }
@@ -40,6 +40,15 @@ module Hecks
40
40
  # state lands in). Double-checked locking against a dedicated mutex
41
41
  # — never `@saga_mutex` — see `Registry#initialize`'s own comment for
42
42
  # why reusing that one would deadlock.
43
+ # Resolves and memoizes the adapter a domain's sagas persist through.
44
+ #
45
+ # @param domain [String, Symbol] the domain to resolve saga persistence for
46
+ # @return [Adapters::Heki, Adapters::Postgres, Adapters::Sqlite, Adapters::D1,
47
+ # Ports::Persistence::Plugins::Era::PostgresEra, Ports::Persistence::NullSagaStore]
48
+ # the same adapter instance the domain's anchor aggregate persists through, when it
49
+ # implements `save_saga`; `NULL_SAGA_STORE` for a domain with no anchor aggregate, an
50
+ # adapter that does not implement the capability, a `RemoteRuntime`-shaped adapter,
51
+ # or a `Runtime::WiringError` resolving the anchor's own bind
43
52
  def saga_persistence(domain)
44
53
  key = domain.to_s
45
54
  @saga_persistence[key] || @saga_persistence_mutex.synchronize do
@@ -67,6 +76,8 @@ module Hecks
67
76
  # points inside `@saga_mutex.synchronize` blocks (`saga_interpreter.
68
77
  # rb`), which genuinely do and are guarded accordingly.
69
78
  # rubocop:disable-next Hecks/ThreadSharedIvarMutation
79
+ #
80
+ # @return [Hecks::Runtime::Registry] self
70
81
  def rehydrate_sagas!
71
82
  @hecksagons.each_key do |domain|
72
83
  saga_persistence(domain).each_saga do |process_manager, correlation, state, memory, completed_compensations = []|