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
@@ -1,4 +1,6 @@
1
1
  require_relative "../naming"
2
+ require_relative "../runtime/caller"
3
+ require_relative "../ports/authorization"
2
4
 
3
5
  module Hecks
4
6
  module Facade
@@ -41,7 +43,7 @@ module Hecks
41
43
  # @param key [Symbol, String] the attribute name
42
44
  # @return [Object, nil] the value held in state (a scalar, a value object, a list,
43
45
  # or a referenced record's id); `nil` when the field is unset or not in state
44
- def [](key) = @state[key.to_sym]
46
+ def [](key) = redacted(key.to_sym)
45
47
 
46
48
  # Answers the record's state as a plain Hash with the bare identity under `:id`.
47
49
  #
@@ -59,7 +61,7 @@ module Hecks
59
61
  #
60
62
  # @return [Hash{Symbol => Object}] a new Hash of every state field by attribute
61
63
  # name, plus `:id` holding the identity String
62
- def to_h = @state.merge(id: @id)
64
+ def to_h = @state.to_h { |key, _| [key, redacted(key)] }.merge(id: @id)
63
65
 
64
66
  # Names the aggregate this record belongs to, in the form every dispatch verb and
65
67
  # event is addressed by.
@@ -120,7 +122,7 @@ module Hecks
120
122
  # written yet
121
123
  # @raise [NoMethodError] if `name` is neither a key in state nor a declared field
122
124
  def method_missing(name, *args, **kwargs, &)
123
- return @state[name] if @state.key?(name) || reader?(name)
125
+ return redacted(name) if @state.key?(name) || reader?(name)
124
126
 
125
127
  super
126
128
  end
@@ -133,6 +135,77 @@ module Hecks
133
135
 
134
136
  def repository = @dispatcher.registry.repository(@domain, @ir)
135
137
 
138
+ # One field's value, with any Privacy::Marking-flagged leaf masked
139
+ # out unless the ambient caller holds a live Governance grant of
140
+ # the marking's own `readable_by` — the read-side half of the
141
+ # Privacy framework member (lib/hecks/framework/bluebook/
142
+ # privacy.bluebook): a marking's presence is what makes a read
143
+ # redacted, not a separate flag this class carries itself.
144
+ #
145
+ # ALWAYS THE STRONG CHECK, never the weak string-only fallback
146
+ # `CommandRules::Authorization#refuse_role_mismatch` allows an
147
+ # unidentified caller — a read gate gone wrong is a leak, not a
148
+ # refused command, so an ambient caller with no `actor_id` (or no
149
+ # caller at all) is masked here, full stop, rather than waved
150
+ # through the way a self-asserted `role` string is for a command.
151
+ #
152
+ # ONE LEVEL OF NESTING ONLY — `attendee.medications` masks inside
153
+ # the returned `Runtime::Value` via its own `#with`; a marking two
154
+ # levels deep is not supported and is left unmasked rather than
155
+ # silently mishandled, since nothing in this corpus needs it yet.
156
+ #
157
+ # @param field [Symbol] the state key being read
158
+ # @return [Object, nil] `@state[field]`, or a copy with the marked leaf replaced by
159
+ # the literal String `"[redacted]"` when the caller is not authorized to see it
160
+ def redacted(field)
161
+ raw = @state[field]
162
+ rows = marked_paths.select { |row| row[:attribute_path][:value].to_s.split(".", 2).first == field.to_s }
163
+ return raw if rows.empty?
164
+
165
+ rows.each do |row|
166
+ path = row[:attribute_path][:value].to_s
167
+ next if authorized_for?(row[:readable_by][:value].to_s)
168
+
169
+ segments = path.split(".", 2)
170
+ if segments.size == 1
171
+ raw = "[redacted]"
172
+ elsif raw.is_a?(Runtime::Value)
173
+ raw = raw.with(segments[1], "[redacted]")
174
+ end
175
+ end
176
+
177
+ raw
178
+ end
179
+
180
+ # Every Privacy::Marking declared for this record's own aggregate — `[]` when the
181
+ # Privacy framework member is not attached in this boot at all, checked once and
182
+ # cheaply rather than paying for a query dispatch every read on a domain that never
183
+ # attached Privacy.
184
+ #
185
+ # @return [Array<Hash>] `Privacy::Marking.ForDomain`'s own rows for this `fqn`
186
+ def marked_paths
187
+ return @marked_paths if defined?(@marked_paths)
188
+ return @marked_paths = [] unless @dispatcher.registry.bluebook("Privacy")
189
+
190
+ @marked_paths = @dispatcher.query("Privacy::Marking.ForDomain", domain: fqn)
191
+ end
192
+
193
+ # Whether the ambient caller holds a live Governance grant of `role`, over this
194
+ # record's own domain — `false` outright for an unidentified caller or a domain
195
+ # with no authorization provider attached, never the weak fallback a command's own
196
+ # role check allows (see `redacted`'s own header for why).
197
+ #
198
+ # @param role [String] the marking's own `readable_by`
199
+ # @return [Boolean] true only for an identified caller holding a live grant
200
+ def authorized_for?(role)
201
+ caller = Runtime::Caller.current
202
+ return false unless caller&.actor_id
203
+ return false unless @dispatcher.registry.authorization_provider_for(@domain)
204
+
205
+ Ports::Authorization.holds_role?(@dispatcher.registry, actor_id: caller.actor_id, role: role,
206
+ as_of: caller.as_of, scope: caller.scope)
207
+ end
208
+
136
209
  def reader?(name)
137
210
  !@ir.attribute(name).nil? || @ir.lifecycle&.field&.to_sym == name
138
211
  end
@@ -1,6 +1,7 @@
1
1
  require_relative "../../bluebook/dsl/hecksagon_builder"
2
2
  require_relative "../../bluebook/dsl/domain_port_builder"
3
3
  require_relative "../../bluebook/dsl/const_shim"
4
+ require_relative "../../bluebook/dsl/binding_proxy"
4
5
  require_relative "../../bluebook/hexagon"
5
6
  require_relative "../handle"
6
7
  require_relative "../../naming"
@@ -164,6 +165,13 @@ module Hecks
164
165
  end
165
166
 
166
167
  door.define_singleton_method(:method_missing) do |verb, *args, **kwargs, &block|
168
+ # A BARE CALL starts a Privacy marking chain — see
169
+ # `Bluebook::DSL::BindingProxy#method_missing`'s own header;
170
+ # this is the same mechanism, reached when the constant is
171
+ # already a real, installed door (a second boot in-process)
172
+ # rather than a `.hecksagon`-parse-time `BindingProxy`.
173
+ return Bluebook::DSL::AttributePath.new(fqn, [verb.to_s]) if args.empty? && kwargs.empty? && !block
174
+
167
175
  collector = Bluebook::DSL::HecksagonBuilder.collector
168
176
  return super(verb, *args, **kwargs, &block) unless collector
169
177
 
@@ -4,6 +4,9 @@ require_relative "../naming"
4
4
  require_relative "value_object_shape"
5
5
 
6
6
  module Hecks
7
+ # The forms surface's own namespace — the `expose` DSL, the IR->HTML
8
+ # renderers, and the Rack app bridging them; see `forms.rb`'s own header,
9
+ # its primary opening, for the fuller design this implements.
7
10
  module Forms
8
11
  # One resolved field, ready for a renderer to turn into markup. A leaf
9
12
  # scalar carries `kind`/`options`/etc; a `:group` or `:list` carries
@@ -9,6 +9,15 @@ module Hecks
9
9
  # `prefers-color-scheme` alone; nothing here reads a cookie or a query
10
10
  # param for it, so it is never wrong for the browser rendering it.
11
11
  module Page
12
+ # Wraps one page's own body HTML in the shared shell: doctype, head, nav, footer,
13
+ # inline styles and script.
14
+ #
15
+ # @param title [String] the page title, escaped into `<title>` and the browser tab
16
+ # @param body [String] the page's own body markup, inserted unescaped inside `<main>`
17
+ # @param breadcrumbs [Array<Array(String, String), Array(String, nil)>] each
18
+ # `[label, href]` pair, in order; the last pair's `href` should be `nil` for the
19
+ # current page
20
+ # @return [String] the complete HTML document
12
21
  def self.render(title:, body:, breadcrumbs: [])
13
22
  <<~HTML
14
23
  <!doctype html>
@@ -36,6 +45,11 @@ module Hecks
36
45
  HTML
37
46
  end
38
47
 
48
+ # Renders the breadcrumb trail as a `<nav>`, each crumb a link except the last.
49
+ #
50
+ # @param crumbs [Array<Array(String, String), Array(String, nil)>] each
51
+ # `[label, href]` pair; a `nil` href renders as the current, unlinked page
52
+ # @return [String] the `<nav>` markup, HTML-escaped; `""` when `crumbs` is empty
39
53
  def self.breadcrumbs_html(crumbs)
40
54
  return "" if crumbs.empty?
41
55
 
@@ -22,6 +22,13 @@ module Hecks
22
22
  module PortArgument
23
23
  module_function
24
24
 
25
+ # Reads the `-p`/`--port` argument out of an argv array.
26
+ #
27
+ # @param argv [Array<String>] the command-line arguments, ARGV-shaped
28
+ # @param default [Integer] the port to use when neither spelling appears at all
29
+ # @return [Array(Integer, nil), Array(nil, String)] `[port, nil]` on a clean
30
+ # parse, or `[nil, message]` when an explicit port was given but isn't a
31
+ # real port number
25
32
  def parse(argv, default: 4567)
26
33
  equals = argv.find { |arg| arg.start_with?("--port=") }
27
34
  return resolve(equals.split("=", 2).last) if equals
@@ -32,6 +39,11 @@ module Hecks
32
39
  resolve(argv[index + 1])
33
40
  end
34
41
 
42
+ # Validates and converts a raw `-p`/`--port` argument value.
43
+ #
44
+ # @param value [String, nil] the text following `-p`/`--port`, or nil if none
45
+ # @return [Array(Integer, nil), Array(nil, String)] `[port, nil]` if `value`
46
+ # is a whole number between 1 and 65535, or `[nil, message]` otherwise
35
47
  def resolve(value)
36
48
  return [nil, "-p/--port requires a value"] if value.nil? || value.empty?
37
49
  return [nil, "-p/--port must be a whole number, got #{value.inspect}"] unless value.match?(/\A\d+\z/)
@@ -20,6 +20,21 @@ module Hecks
20
20
  # exactly the same kind of link on submit; and the results table once
21
21
  # a request actually supplies parameters.
22
22
  module QueryFormRenderer
23
+ # Renders one query's whole GET view: header, canonical link, quick links, filter
24
+ # form, any error, results, and the inspect panel.
25
+ #
26
+ # @param registry [Runtime::Registry] the booted registry, for resolving reference
27
+ # fields' target aggregates
28
+ # @param domain [String] the owning chapter's name
29
+ # @param aggregate [Bluebook::Aggregate] the aggregate the query belongs to
30
+ # @param query [Bluebook::Query] the query being rendered
31
+ # @param action [String] the form's `action` URL and the canonical link's base
32
+ # @param params [Hash] the request's own query params, pre-filling the filter form
33
+ # @param results [Array<Hash>, nil] the query's own answer rows; `nil` renders no
34
+ # results section
35
+ # @param error [StandardError, nil] a raised error to show in the error banner;
36
+ # `nil` renders none
37
+ # @return [String] the page body markup
23
38
  def self.render(registry:, domain:, aggregate:, query:, action:, params: {}, results: nil, error: nil)
24
39
  fields = query.attributes.map { |a| FieldShape.resolve(a, aggregate: aggregate) }
25
40
  reference_options = ReferenceOptions.collect(registry, domain, fields)
@@ -35,6 +50,12 @@ module Hecks
35
50
  HTML
36
51
  end
37
52
 
53
+ # Renders the query's title, description and badges.
54
+ #
55
+ # @param domain [String] the owning chapter's name
56
+ # @param aggregate [Bluebook::Aggregate] the aggregate the query belongs to
57
+ # @param query [Bluebook::Query] the query being rendered
58
+ # @return [String] the header markup
38
59
  def self.header(domain, aggregate, query)
39
60
  <<~HTML
40
61
  <h1>#{Escape.html("#{domain}::#{aggregate.hecks_name}.#{query.hecks_name}")}</h1>
@@ -43,12 +64,22 @@ module Hecks
43
64
  HTML
44
65
  end
45
66
 
67
+ # Renders the query's declared badges — today, just its `limit` when it declares one.
68
+ #
69
+ # @param query [Bluebook::Query] the query being rendered
70
+ # @return [String] the badge markup; `""` when the query declares no `limit`
46
71
  def self.badges(query)
47
72
  parts = []
48
73
  parts << %(<span class="badge">limit #{Escape.html(query.limit.to_h[:value])}</span>) if query.limit
49
74
  parts.join
50
75
  end
51
76
 
77
+ # Renders the query's plain-GET canonical link template, one `path={path}` per
78
+ # parameter.
79
+ #
80
+ # @param action [String] the query's base URL
81
+ # @param fields [Array<Forms::Field>] the resolved field tree naming the parameters
82
+ # @return [String] the help text and the templated link markup
52
83
  def self.canonical_link(action, fields)
53
84
  paths = Params.paths(fields)
54
85
  template = paths.empty? ? action : "#{action}?#{paths.map { |path| "#{path}={#{path}}" }.join('&')}"
@@ -64,6 +95,12 @@ module Hecks
64
95
  # would mean a cross product of links, which reads as noise rather
65
96
  # than help. A query with more than one is still fully reachable
66
97
  # through the filter form below.
98
+ #
99
+ # @param action [String] the query's base URL
100
+ # @param fields [Array<Forms::Field>] the resolved field tree to search for a
101
+ # `:select` or `:radio` field
102
+ # @return [String] one clickable link per option of the first closed-set field
103
+ # found; `""` when no field is `:select` or `:radio`
67
104
  def self.quick_links(action, fields)
68
105
  field = fields.find { |f| %i[select radio].include?(f.kind) }
69
106
  return "" unless field
@@ -75,6 +112,14 @@ module Hecks
75
112
  %(<div class="example-links">#{links.join}</div>)
76
113
  end
77
114
 
115
+ # Renders the GET filter form covering every declared parameter.
116
+ #
117
+ # @param action [String] the form's `action` URL
118
+ # @param fields [Array<Forms::Field>] the resolved field tree to render
119
+ # @param params [Hash] the request's own query params, pre-filling each field
120
+ # @param reference_options [Hash{String => Array<Array(String, String)>, nil}] each
121
+ # `:reference` field's own dropdown options, as `ReferenceOptions.collect` returns
122
+ # @return [String] the form markup; `""` when the query declares no parameters
78
123
  def self.filter_form(action, fields, params, reference_options)
79
124
  return "" if fields.empty?
80
125
 
@@ -86,6 +131,10 @@ module Hecks
86
131
  HTML
87
132
  end
88
133
 
134
+ # Renders a raised error as a dismissable-looking alert banner.
135
+ #
136
+ # @param error [StandardError, nil] the error to show
137
+ # @return [String] the banner markup; `""` when `error` is `nil`
89
138
  def self.error_banner(error)
90
139
  return "" unless error
91
140
 
@@ -93,12 +142,26 @@ module Hecks
93
142
  "#{Escape.html(error.message)}</p></div>"
94
143
  end
95
144
 
145
+ # Renders the results heading and table, once a request actually supplied
146
+ # parameters.
147
+ #
148
+ # @param aggregate [Bluebook::Aggregate] the aggregate whose fields shape the table
149
+ # @param results [Array<Hash>, nil] the query's own answer rows
150
+ # @param domain [String] the owning chapter's name, for building record links
151
+ # @return [String] the results section markup; `""` when `results` is `nil`
96
152
  def self.results_section(aggregate, results, domain)
97
153
  return "" unless results
98
154
 
99
155
  "<h2>Results (#{results.size})</h2>#{RecordTable.render(aggregate, results, domain: domain)}"
100
156
  end
101
157
 
158
+ # Renders a collapsed panel with the query's own parameter list and raw IR JSON.
159
+ #
160
+ # @param domain [String] the owning chapter's name
161
+ # @param aggregate [Bluebook::Aggregate] the aggregate the query belongs to
162
+ # @param query [Bluebook::Query] the query being rendered
163
+ # @param fields [Array<Forms::Field>] the resolved field tree, for the parameter list
164
+ # @return [String] the `<details>` panel markup
102
165
  def self.inspect_panel(domain, aggregate, query, fields)
103
166
  verb = "#{domain}::#{aggregate.hecks_name}.#{query.hecks_name}"
104
167
  paths = Params.paths(fields)
@@ -11,6 +11,12 @@ module Hecks
11
11
  # do (facade/surface/aggregate_door.rb) — so they exist for every
12
12
  # aggregate whether or not its bluebook declared a query at all.
13
13
  module RecordRenderer
14
+ # Renders the index page listing every record of one aggregate.
15
+ #
16
+ # @param registry [Runtime::Registry] the booted registry holding the domain
17
+ # @param domain [String, Symbol] the domain name
18
+ # @param aggregate [Bluebook::Aggregate] the aggregate whose records are listed
19
+ # @return [String] the index page's rendered HTML
14
20
  def self.index(registry:, domain:, aggregate:)
15
21
  instances = registry.repository(domain, aggregate).all
16
22
  <<~HTML
@@ -23,6 +29,12 @@ module Hecks
23
29
  HTML
24
30
  end
25
31
 
32
+ # Renders the "+ Command" buttons for every creating command.
33
+ #
34
+ # @param domain [String, Symbol] the domain name
35
+ # @param aggregate [Bluebook::Aggregate] the aggregate whose creating commands
36
+ # are linked
37
+ # @return [String] HTML action buttons, or `""` if the aggregate declares none
26
38
  def self.creating_links(domain, aggregate)
27
39
  creators = aggregate.commands.select(&:creates?)
28
40
  return "" if creators.empty?
@@ -34,6 +46,15 @@ module Hecks
34
46
  %(<div class="actions">#{links.join}</div>)
35
47
  end
36
48
 
49
+ # Renders one record's state page: its current fields, plus the commands and
50
+ # queries it can dispatch next.
51
+ #
52
+ # @param registry [Runtime::Registry] the booted registry holding the domain
53
+ # @param domain [String, Symbol] the domain name
54
+ # @param aggregate [Bluebook::Aggregate] the aggregate the record belongs to
55
+ # @param id [String, Object] the record's identity
56
+ # @return [String, nil] the record page's rendered HTML, or nil if no record
57
+ # has that id
37
58
  def self.show(registry:, domain:, aggregate:, id:)
38
59
  instance = registry.repository(domain, aggregate).find(id)
39
60
  return nil unless instance
@@ -50,6 +71,10 @@ module Hecks
50
71
  HTML
51
72
  end
52
73
 
74
+ # Renders a record's state as an HTML field table.
75
+ #
76
+ # @param state [Hash{Symbol => Object}] the record's stored attribute values
77
+ # @return [String] an HTML table of the state's fields, values escaped
53
78
  def self.state_table(state)
54
79
  rows = state.map do |key, value|
55
80
  "<tr><th>#{Escape.html(Humanize.label(key.to_s))}</th><td>#{Escape.html(render_value(value))}</td></tr>"
@@ -60,6 +85,10 @@ module Hecks
60
85
  # `Value.materialize` first — a stored field is a `Runtime::Value`
61
86
  # wherever its attribute is a value object, not a plain Hash (see
62
87
  # record_table.rb's own note on the same read).
88
+ #
89
+ # @param value [Object] a stored field's value, possibly a `Runtime::Value`
90
+ # @return [String] the value rendered as text: a Hash as `"key: value, ..."`
91
+ # pairs, an Array `;`-joined, anything else its own `to_s`
63
92
  def self.render_value(value)
64
93
  case (value = Runtime::Value.materialize(value))
65
94
  when Hash then value.map { |k, v| "#{k}: #{render_value(v)}" }.join(", ")
@@ -72,6 +101,14 @@ module Hecks
72
101
  # not apply from the record's current state — the same rule
73
102
  # `Rules#admissible_transition` enforces at dispatch, read here so a
74
103
  # link that would only refuse is never offered in the first place.
104
+ #
105
+ # @param domain [String, Symbol] the domain name
106
+ # @param aggregate [Bluebook::Aggregate] the aggregate the record belongs to
107
+ # @param id [String, Object] the record's identity
108
+ # @param current_state [String, nil] the record's current lifecycle state, or
109
+ # nil if the aggregate has no lifecycle field or it is unset
110
+ # @return [String] an HTML list of admissible command links, or a message
111
+ # when no command currently applies
75
112
  def self.command_links(domain, aggregate, id, current_state)
76
113
  commands = aggregate.commands.reject(&:creates?).select { |cmd| applies?(aggregate, cmd, current_state) }
77
114
  return "<p><em>No commands act on an existing #{Escape.html(aggregate.hecks_name)}.</em></p>" if commands.empty?
@@ -88,6 +125,14 @@ module Hecks
88
125
  %(<ul class="verb-list">#{items.join}</ul>)
89
126
  end
90
127
 
128
+ # Whether the aggregate's lifecycle admits dispatching `command` from
129
+ # `current_state`.
130
+ #
131
+ # @param aggregate [Bluebook::Aggregate] the aggregate the command belongs to
132
+ # @param command [Bluebook::Command] the command to check
133
+ # @param current_state [String, nil] the record's current lifecycle state
134
+ # @return [Boolean] true if the aggregate declares no lifecycle transitions for
135
+ # this command, or one of them admits `current_state`
91
136
  def self.applies?(aggregate, command, current_state)
92
137
  transitions = aggregate.lifecycle&.transitions_for(command.hecks_name) || []
93
138
  return true if transitions.empty?
@@ -95,6 +140,12 @@ module Hecks
95
140
  transitions.any? { |t| t.from.nil? || Array(t.from).map(&:to_s).include?(current_state.to_s) }
96
141
  end
97
142
 
143
+ # Renders the "Queries" link list for an aggregate's own queries.
144
+ #
145
+ # @param domain [String, Symbol] the domain name
146
+ # @param aggregate [Bluebook::Aggregate] the aggregate whose queries are linked
147
+ # @return [String] an HTML section listing every query, or `""` if the
148
+ # aggregate declares none
98
149
  def self.query_links(domain, aggregate)
99
150
  return "" if aggregate.queries.empty?
100
151
 
@@ -105,6 +156,13 @@ module Hecks
105
156
  %(<h2>Queries</h2><ul class="verb-list">#{items.join}</ul>)
106
157
  end
107
158
 
159
+ # Renders the full command-and-query index for an aggregate, independent of
160
+ # any one record's state.
161
+ #
162
+ # @param domain [String, Symbol] the domain name
163
+ # @param aggregate [Bluebook::Aggregate] the aggregate whose verbs are listed
164
+ # @return [String] an HTML section listing every non-creating command and every
165
+ # query, or `""` if the aggregate declares neither
108
166
  def self.verb_list(domain, aggregate)
109
167
  commands = aggregate.commands.reject(&:creates?)
110
168
  return "" if commands.empty? && aggregate.queries.empty?
@@ -19,12 +19,26 @@ module Hecks
19
19
  # query's own results (query_form_renderer.rb) — the same records
20
20
  # read the same columns either way.
21
21
  module RecordTable
22
+ # Picks which fields to show as columns: identity, lifecycle state, then scalar
23
+ # attributes, capped at seven.
24
+ #
25
+ # @param aggregate [Bluebook::Aggregate] the aggregate whose records will be shown
26
+ # @return [Array<Symbol>] up to 7 unique field names, in that order; excludes
27
+ # reference and list attributes
22
28
  def self.columns(aggregate)
23
29
  lifecycle = aggregate.lifecycle&.field
24
30
  scalars = aggregate.attributes.reject { |a| a.reference? || a.list? }.map(&:name)
25
31
  [*aggregate.identity_heads, lifecycle, *scalars].compact.uniq.first(7)
26
32
  end
27
33
 
34
+ # Renders a list of records as an HTML table, one row per record.
35
+ #
36
+ # @param aggregate [Bluebook::Aggregate] the aggregate the records belong to
37
+ # @param instances [Array<Runtime::Instance, Forms::Record>] the records to render;
38
+ # anything answering `#id`/`#state` works
39
+ # @param domain [String] the owning chapter's name, for building each row's link
40
+ # @return [String] the table markup; `"<p><em>No records.</em></p>"` when
41
+ # `instances` is empty
28
42
  def self.render(aggregate, instances, domain:)
29
43
  cols = columns(aggregate)
30
44
  head = (["id"] + cols).map { |name| "<th>#{Escape.html(Humanize.label(name.to_s))}</th>" }.join
@@ -39,6 +53,13 @@ module Hecks
39
53
  HTML
40
54
  end
41
55
 
56
+ # Renders one record's own table row, linked to its show page.
57
+ #
58
+ # @param instance [Runtime::Instance, Forms::Record] the record to render
59
+ # @param aggregate [Bluebook::Aggregate] the aggregate the record belongs to
60
+ # @param cols [Array<Symbol>] the column field names, as `columns` returns them
61
+ # @param domain [String] the owning chapter's name, for building the row's link
62
+ # @return [String] the `<tr>` markup
42
63
  def self.row(instance, aggregate, cols, domain)
43
64
  cells = cols.map { |name| "<td>#{Escape.html(cell(instance, name))}</td>" }.join
44
65
  # L12 — the id is free-form (S3): percent-encoded as the path
@@ -50,6 +71,12 @@ module Hecks
50
71
  "<tr><td><a href=\"#{Escape.attr(href)}\">#{Escape.html(instance.id)}</a></td>#{cells}</tr>"
51
72
  end
52
73
 
74
+ # Reads one field's value for display, unwrapped to a single cell value.
75
+ #
76
+ # @param instance [Runtime::Instance, Forms::Record] the record to read
77
+ # @param name [Symbol] the field name to read
78
+ # @return [Object] the field's own value; for a Hash-shaped value object, its
79
+ # first member's value; `""` for `nil`
53
80
  def self.cell(instance, name)
54
81
  # `state` holds `Runtime::Value` wherever an attribute is a value
55
82
  # object, not a plain Hash — `.materialize` is the runtime's own
@@ -7,12 +7,27 @@ module Hecks
7
7
  # resolve against the same repository the same way, and neither word
8
8
  # owns this more than the other.
9
9
  module ReferenceOptions
10
+ # Collects every `:reference` field's own dropdown options out of a resolved field
11
+ # tree.
12
+ #
13
+ # @param registry [Runtime::Registry] the booted registry to read repositories from
14
+ # @param domain [String] the owning chapter's name
15
+ # @param fields [Array<Forms::Field>] the resolved field tree to search
16
+ # @return [Hash{String => Array<Array(String, String)>, nil}] one entry per
17
+ # `:reference` field found, keyed by its `path`; `nil` when the field's target
18
+ # could not be resolved or its options could not be read
10
19
  def self.collect(registry, domain, fields)
11
20
  targets = {}
12
21
  walk(fields) { |field| targets[field.path] = field.target_aggregate if field.kind == :reference }
13
22
  targets.transform_values { |aggregate| aggregate && options_for(registry, domain, aggregate) }
14
23
  end
15
24
 
25
+ # Visits every field in a tree, depth-first, including each `:list`/`:group`
26
+ # field's own children.
27
+ #
28
+ # @param fields [Array<Forms::Field>] the field tree to walk
29
+ # @yieldparam field [Forms::Field] each field visited, parent before children
30
+ # @return [void]
16
31
  def self.walk(fields, &block)
17
32
  fields.each do |field|
18
33
  block.call(field)
@@ -20,6 +35,15 @@ module Hecks
20
35
  end
21
36
  end
22
37
 
38
+ # Reads up to 200 records' ids as `<select>` options, for one reference field's
39
+ # target aggregate.
40
+ #
41
+ # @param registry [Runtime::Registry] the booted registry to read the repository from
42
+ # @param domain [String] the owning chapter's name
43
+ # @param aggregate [Bluebook::Aggregate] the referenced aggregate to list
44
+ # @return [Array<Array(String, String)>, nil] up to 200 `[id, id]` pairs; `nil` when
45
+ # the repository cannot be resolved (a wiring gap degrades to a plain text id
46
+ # input, not a 500)
23
47
  def self.options_for(registry, domain, aggregate)
24
48
  registry.repository(domain, aggregate).all.first(200).map { |instance| [instance.id, instance.id] }
25
49
  rescue StandardError
@@ -18,6 +18,8 @@ module Hecks
18
18
  # every money-shaped VO in the corpus spells it exactly this way,
19
19
  # the two-attribute convention the language itself never enforces
20
20
  # but every real chapter follows.
21
+ # @param value_object [Bluebook::ValueObject] the value object to classify
22
+ # @return [Boolean] true if its attributes are exactly `cents` and `currency`
21
23
  def money?(value_object)
22
24
  value_object.attributes.map { |a| a.name.to_s }.sort == %w[cents currency]
23
25
  end
@@ -27,6 +29,10 @@ module Hecks
27
29
  # [[feedback_name_the_scalar_field]]'s own reasoning, shared here
28
30
  # rather than re-decided per caller. Returns the sole attribute,
29
31
  # or nil for anything else.
32
+ #
33
+ # @param value_object [Bluebook::ValueObject] the value object to inspect
34
+ # @return [Bluebook::Attribute, nil] its one declared attribute, or nil if
35
+ # it declares zero or more than one
30
36
  def sole_attribute(value_object)
31
37
  return nil unless value_object.attributes.size == 1
32
38
 
@@ -38,6 +44,10 @@ module Hecks
38
44
  # money-shaped (money's own two members are handled by `money?`
39
45
  # instead, since which one governs ordering is a money-specific
40
46
  # decision, not a general "pick the first number" one).
47
+ #
48
+ # @param value_object [Bluebook::ValueObject] the value object to inspect
49
+ # @return [Bluebook::Attribute, nil] its first Integer- or Float-typed
50
+ # attribute, or nil if it declares none
41
51
  def numeric_member(value_object)
42
52
  value_object.attributes.find { |a| %w[Integer Float].include?(a.type.to_s) }
43
53
  end
data/lib/hecks/fqn.rb CHANGED
@@ -8,10 +8,35 @@ module Hecks
8
8
 
9
9
  attr_reader :realm, :domain, :version, :aggregate, :verb, :kind
10
10
 
11
+ # Builds the FQN of one command on an aggregate.
12
+ #
13
+ # @param realm [String, nil] deployment identity from the world, or nil for an
14
+ # unrealmed address
15
+ # @param domain [String] the domain name
16
+ # @param aggregate [String] the aggregate name the command belongs to
17
+ # @param command [String] the command's PascalCase verb
18
+ # @param version [String, nil] the pinned domain version, or nil for the
19
+ # world's configured latest alias
20
+ # @return [Fqn] the command address
21
+ # @raise [Fqn::Invalid] if any segment is empty or contains a separator, or if
22
+ # `command` is not a valid PascalCase command name
11
23
  def self.command(realm:, domain:, aggregate:, command:, version: nil)
12
24
  new(realm: realm, domain: domain, version: version, aggregate: aggregate, verb: command, kind: :command)
13
25
  end
14
26
 
27
+ # Builds the FQN of one query, either on an aggregate or domain-level.
28
+ #
29
+ # @param realm [String, nil] deployment identity from the world, or nil for an
30
+ # unrealmed address
31
+ # @param domain [String] the domain name
32
+ # @param query [String] the query's snake_case verb
33
+ # @param aggregate [String, nil] the aggregate the query belongs to, or nil for
34
+ # a domain-level read model
35
+ # @param version [String, nil] the pinned domain version, or nil for the
36
+ # world's configured latest alias
37
+ # @return [Fqn] the query address
38
+ # @raise [Fqn::Invalid] if any segment is empty or contains a separator, or if
39
+ # `query` is not a valid snake_case query name
15
40
  def self.query(realm:, domain:, query:, aggregate: nil, version: nil)
16
41
  new(realm: realm, domain: domain, version: version, aggregate: aggregate, verb: query, kind: :query)
17
42
  end
@@ -22,6 +47,13 @@ module Hecks
22
47
  # kind) the step before it derived; splitting would mean threading all of
23
48
  # them back out as parameters/returns between new methods, for no
24
49
  # readability gain over reading the pipeline top to bottom once.
50
+ #
51
+ # @param text [String] a `Realm::Domain::Aggregate.verb` address, with domain
52
+ # optionally `@version`-pinned and aggregate optional for a domain-level query
53
+ # @return [Fqn] the parsed address
54
+ # @raise [Fqn::Invalid] if `text` is not shaped like a FQN, its verb is neither
55
+ # PascalCase nor snake_case, its domain version is malformed, or it names a
56
+ # domain-level command
25
57
  # rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
26
58
  def self.parse(text)
27
59
  head, separator, verb = text.to_s.rpartition(".")
@@ -53,9 +85,28 @@ module Hecks
53
85
  new(realm: realm, domain: domain, version: version, aggregate: aggregate, verb: verb, kind: kind)
54
86
  end
55
87
 
88
+ # Whether `name` is a valid command verb: PascalCase.
89
+ #
90
+ # @param name [String, Symbol, #to_s] the candidate verb
91
+ # @return [Boolean] true if `name` matches the PascalCase command shape
56
92
  def self.command_name?(name) = /\A[A-Z][A-Za-z0-9]*\z/.match?(name.to_s)
93
+
94
+ # Whether `name` is a valid query verb: snake_case.
95
+ #
96
+ # @param name [String, Symbol, #to_s] the candidate verb
97
+ # @return [Boolean] true if `name` matches the snake_case query shape
57
98
  def self.query_name?(name) = /\A[a-z][a-z0-9_]*\z/.match?(name.to_s)
58
99
 
100
+ # @param realm [String, nil] deployment identity from the world, or nil for an
101
+ # unrealmed address
102
+ # @param domain [String] the domain name
103
+ # @param aggregate [String, nil] the aggregate name, or nil for a domain-level query
104
+ # @param verb [String] the command or query verb
105
+ # @param kind [Symbol, String] `:command` or `:query`
106
+ # @param version [String, nil] the pinned domain version, or nil for the
107
+ # world's configured latest alias
108
+ # @raise [Fqn::Invalid] if any segment is empty or contains a separator, or if
109
+ # `verb` does not match the shape required by `kind`
59
110
  def initialize(realm:, domain:, aggregate:, verb:, kind:, version: nil)
60
111
  @realm = realm && segment(realm, "realm")
61
112
  @domain = segment(domain, "domain")
@@ -68,7 +119,14 @@ module Hecks
68
119
  raise Invalid, "#{@kind} FQN has an invalid verb #{@verb.inspect}" unless valid
69
120
  end
70
121
 
122
+ # Whether this address names a command.
123
+ #
124
+ # @return [Boolean] true if this address's kind is `:command`
71
125
  def command? = @kind == :command
126
+
127
+ # Whether this address names a query.
128
+ #
129
+ # @return [Boolean] true if this address's kind is `:query`
72
130
  def query? = @kind == :query
73
131
 
74
132
  def to_s