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
@@ -10,10 +10,11 @@ module Hecks
10
10
  # Dispatcher, its own adapter instances) rather than one shared
11
11
  # process switching connections mid-dispatch.
12
12
  #
13
- # That last part is the finding this module encodes. The project
14
- # register (Bluebook::ProjectRegister) already resolves an address's
15
- # realm to a dispatcher at registration time — Router#resolve looks
16
- # the FQN up in one flat table keyed by realm::domain::aggregate.verb,
13
+ # ## Why no ambient "current tenant"
14
+ #
15
+ # The project register (Bluebook::ProjectRegister) already resolves an
16
+ # address's realm to a dispatcher at registration time — Router#resolve
17
+ # looks the FQN up in one flat table keyed by realm::domain::aggregate.verb,
17
18
  # and each entry already carries its own dispatcher from its own
18
19
  # boot. So "which tenant" is decided once, at boot/registration time
19
20
  # (which of possibly many boots of the same directory a request's
@@ -23,8 +24,9 @@ module Hecks
23
24
  # already gives for free — each tenant's own PostgresEra instance
24
25
  # is its own connection, held for the life of that boot.
25
26
  #
26
- # So `tenant_capable?` asks a narrower question than it might sound:
27
- # not "can this adapter switch tenants," but "does booting this
27
+ # ## What `tenant_capable?` really asks
28
+ #
29
+ # Not "can this adapter switch tenants," but "does booting this
28
30
  # adapter twice, with different settings, for the same directory,
29
31
  # actually keep the two boots' data apart." Memory answers true
30
32
  # trivially — a `@records` Hash is a plain instance variable, and
@@ -41,6 +43,9 @@ module Hecks
41
43
  module TenantCheck
42
44
  module_function
43
45
 
46
+ # Refuses to let `domain` boot for more than one tenant unless every
47
+ # aggregate's resolved persistence adapter is `tenant_capable?`.
48
+ #
44
49
  # A domain is safe to boot for more than one tenant only if every
45
50
  # aggregate's resolved persistence adapter is tenant_capable? — one
46
51
  # ungoverned adapter sharing state across two tenant boots is a
@@ -48,6 +53,12 @@ module Hecks
48
53
  # a second tenant boot of the same directory is trusted, the same
49
54
  # severity EraCheck/refuse_ungoverned_roles! already hold their
50
55
  # own gates to.
56
+ #
57
+ # @param registry [Runtime::Registry] the booted registry to check
58
+ # @param domain [String, Symbol] the domain name to check every aggregate of
59
+ # @return [void]
60
+ # @raise [Runtime::WiringError] if any aggregate in `domain` is bound to an adapter
61
+ # that is not `tenant_capable?`
51
62
  def refuse_unless_tenant_capable!(registry, domain)
52
63
  bluebook = registry.bluebook(domain)
53
64
  return unless bluebook
@@ -67,12 +78,21 @@ module Hecks
67
78
  "or keep #{domain} single-tenant."
68
79
  end
69
80
 
81
+ # Answers whether `adapter_name`'s Ruby implementation keeps two
82
+ # tenants' boots of the same directory from sharing data.
83
+ #
70
84
  # The capability idiom itself — an adapter class that answers
71
85
  # tenant_capable? with true keeps two boots' data apart by
72
86
  # construction (Memory) or by an explicit per-boot isolation
73
87
  # setting (PostgresEra's schema:). Same defensive shape
74
88
  # EraCheck#lineage_capable? already uses: a class that doesn't
75
89
  # respond at all is false, not an error.
90
+ #
91
+ # @param registry [Runtime::Registry] the booted registry the adapter is wired into
92
+ # @param adapter_name [String] the adapter's declared name, such as `"PostgresEra"`
93
+ # @return [Boolean] true when the adapter is registered, has a Ruby implementation,
94
+ # and answers `tenant_capable?` true; false for any other case, including a
95
+ # missing adapter or one whose lookup raises
76
96
  def tenant_capable?(registry, adapter_name)
77
97
  adapter_class = registry.adapters[adapter_name] && registry.adapter_class(adapter_name)
78
98
  adapter_class.respond_to?(:tenant_capable?) && adapter_class.tenant_capable?
@@ -29,6 +29,16 @@ module Hecks
29
29
  module TenantScope
30
30
  module_function
31
31
 
32
+ # Wraps a declared query/read-model spec with its tenant boundary clause, if it has one.
33
+ #
34
+ # @param declared [Bluebook::Query, Bluebook::ReadModel] the declared specification to
35
+ # scope
36
+ # @param args [Hash] the query's arguments, checked for the declared tenant field
37
+ # @return [Bluebook::Query, Bluebook::ReadModel, Runtime::TenantScope::Scoped]
38
+ # `declared` unchanged when it declares no `authorize policy, tenant:`; otherwise a
39
+ # `Scoped` wrapper whose `#wheres` adds the tenant `eq` clause
40
+ # @raise [Runtime::Unauthorized] if `declared` declares a tenant boundary and `args`
41
+ # omits that field
32
42
  def apply(declared, args)
33
43
  tenant = declared.authorization&.tenant
34
44
  return declared unless tenant
@@ -50,11 +60,19 @@ module Hecks
50
60
  # own internal `.wheres` read would bypass this override (see this
51
61
  # module's own header for why).
52
62
  class Scoped < SimpleDelegator
63
+ # @param declared [Bluebook::Query, Bluebook::ReadModel] the specification to wrap,
64
+ # delegated to for everything but `#wheres`
65
+ # @param clause [QuerySpecification::Common::WhereClause] the synthetic tenant `eq`
66
+ # clause to append
53
67
  def initialize(declared, clause)
54
68
  super(declared)
55
69
  @clause = clause
56
70
  end
57
71
 
72
+ # Reads the wrapped specification's where-clauses, with the tenant clause appended.
73
+ #
74
+ # @return [Array<QuerySpecification::Common::WhereClause>] `declared.wheres` with the
75
+ # tenant clause appended
58
76
  def wheres = __getobj__.wheres + [@clause]
59
77
  end
60
78
  end
@@ -12,7 +12,7 @@ module Hecks
12
12
  # typed Value. Extended into Value, so every method here reads as
13
13
  # `Value.for`, `Value.build`, … — `self` is the Value class.
14
14
  module Coercion
15
- # The four SHAPES an attribute's value can take — named here because
15
+ # The four `SHAPES` an attribute's value can take — named here because
16
16
  # `for_attribute` immediately below is the one place that actually
17
17
  # branches on all four, and nowhere else in the language collects
18
18
  # them into a single closed list. `Attribute#list?`/`#optional?`
@@ -34,6 +34,20 @@ module Hecks
34
34
  # about no matter how correct the Ruby below is.
35
35
  SHAPES = %i[scalar list optional composite].freeze
36
36
 
37
+ # Coerces `value` for one of `aggregate`'s declared attributes, by name.
38
+ #
39
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct
40
+ # `name` is looked up on
41
+ # @param name [String, Symbol] the declared attribute name
42
+ # @param value [Object] the raw value to coerce
43
+ # @return [Runtime::Value, Object, nil] `value` unchanged when `aggregate`
44
+ # declares no such attribute; otherwise `for_attribute`'s own result
45
+ # @raise [Runtime::TypeMismatch] if `value` cannot be coerced to the
46
+ # attribute's declared type
47
+ # @raise [Runtime::UnknownArgument] if `value` is a Hash naming a field the
48
+ # attribute's own value-object type does not declare
49
+ # @raise [Runtime::InvariantViolation] if a coerced value object breaks one
50
+ # of its own invariants
37
51
  def for(aggregate, name, value)
38
52
  attribute = aggregate.attribute(name)
39
53
  return value unless attribute
@@ -71,6 +85,30 @@ module Hecks
71
85
  # it does not reach this default-absorbing fallback (QualityControl
72
86
  # BUG#36 — a query's own null VO argument must refuse regardless
73
87
  # of any default, unlike a command's).
88
+ #
89
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct
90
+ # `attribute` is declared on
91
+ # @param attribute [Bluebook::Attribute, nil] the attribute to coerce `value`
92
+ # for; nil is treated as an unknown attribute (no shape to coerce against)
93
+ # @param value [Object] the raw value to coerce
94
+ # @param boundary [Boolean] whether a bare-primitive attribute's own scalar
95
+ # shape is checked; false at the query door, where a declared type names
96
+ # the argument for callers/generators only
97
+ # @param argument [Boolean] whether this is the command/entity/port argument
98
+ # door, where a nil for a non-optional attribute is a left-empty argument
99
+ # (C3.7) rather than ordinary state-assembly nil
100
+ # @return [Runtime::Value, Object, nil] the coerced value: a built `Value`
101
+ # for a composite-typed attribute, a frozen Array for a `has_many`
102
+ # reference or hydrated entity list, a joined identity String for a
103
+ # scalar reference, or `value` passed through unchanged for a bare
104
+ # primitive or an unknown attribute; nil for an absent optional attribute
105
+ # @raise [Runtime::TypeMismatch] if `value` cannot be coerced to the
106
+ # attribute's declared type, or a required reference/attribute is offered
107
+ # as a wrong-shaped value
108
+ # @raise [Runtime::UnknownArgument] if `value` is a Hash naming a field the
109
+ # attribute's own value-object type does not declare
110
+ # @raise [Runtime::InvariantViolation] if a coerced value object breaks one
111
+ # of its own invariants
74
112
  def for_attribute(aggregate, attribute, value, boundary: true, argument: false)
75
113
  return nil_or_missing(aggregate, attribute, value, argument) if attribute.nil? || value.nil?
76
114
  return reference_list(attribute, value) if attribute.list? && attribute.reference?
@@ -145,10 +183,20 @@ module Hecks
145
183
  value
146
184
  end
147
185
 
186
+ # Resolves the value-object class `type` names, searching `aggregate`'s own
187
+ # declarations first, then its chapter's other aggregates.
188
+ #
148
189
  # Aggregate-local value objects remain authoritative, which permits
149
190
  # intentional duplication. An ordinary fact may also name an identity
150
191
  # value object declared on another aggregate; that shape is borrowed
151
192
  # only when every chapter declaration with the name agrees.
193
+ #
194
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct to
195
+ # search first
196
+ # @param type [String, Symbol, #to_s] the declared type name to resolve
197
+ # @return [Class, nil] the `Bluebook::ValueObject` subclass `type` names; nil
198
+ # if `aggregate` declares none, and its chapter's other aggregates disagree
199
+ # on the shape of every same-named one (or declare none either)
152
200
  def value_object_for(aggregate, type)
153
201
  local = aggregate.value_object(type)
154
202
  return local if local
@@ -163,11 +211,21 @@ module Hecks
163
211
  shapes.size == 1 ? matches.first : nil
164
212
  end
165
213
 
214
+ # Coerces a reference-typed attribute's offered value into the target's own
215
+ # canonical identity string.
216
+ #
166
217
  # Retained relationships store canonical target identities, not Ruby
167
218
  # Value wrappers. Raw scalar IDs remain a compatibility input. A named
168
219
  # identity VO omits its minted aggregate field at the command boundary;
169
220
  # a bespoke compound VO may instead name the target heads directly.
170
221
  # Neither form requires reverse-splitting a canonical ID.
222
+ #
223
+ # @param attribute [Bluebook::Attribute] the reference-typed attribute
224
+ # @param value [Object] the offered value: a bare scalar identity, a
225
+ # `Runtime::Value`, or a Hash naming the target's own identity fields
226
+ # @return [String, Object] the joined canonical identity String once every
227
+ # identity part resolves; `value` unchanged otherwise (a bare scalar, an
228
+ # unresolvable target, or a shape `sole_scalar_identity` cannot unwrap)
171
229
  def reference_identity(attribute, value)
172
230
  return value unless value.is_a?(self) || value.is_a?(Hash)
173
231
 
@@ -198,8 +256,8 @@ module Hecks
198
256
  # object, `direct_identity_head`) — an ad hoc wrapper around a
199
257
  # bare scalar under some other field name (`VenueHandle`'s own
200
258
  # `value`, not `Venue`'s own `code`) fails every path lookup, and
201
- # used to fall through to the unresolved `return value` above,
202
- # storing the wrapped Value. That leaves one reference field on
259
+ # without this would fall through to the unresolved `return value`
260
+ # above, storing the wrapped Value. That leaves one reference field on
203
261
  # one aggregate holding two different shapes depending on which
204
262
  # command last wrote it — `Open`'s own bare `reference_to Venue`
205
263
  # argument was never wrapped in the first place (the top guard
@@ -218,6 +276,11 @@ module Hecks
218
276
  # already does for `Value.materialize_unwrapped`'s other callers,
219
277
  # landing on the bare scalar `Open`'s own path already produces
220
278
  # for the identical target field.
279
+ #
280
+ # @param value [Object] the offered reference value to unwrap
281
+ # @param paths [Array<String>] the target's own declared identity paths
282
+ # @return [Object, nil] the unwrapped scalar when `value` is a single-field
283
+ # value object and `paths` names exactly one path; nil otherwise
221
284
  def sole_scalar_identity(value, paths)
222
285
  return nil unless value.is_a?(self) && paths.one?
223
286
  return nil unless value.value_object.sole_attribute
@@ -228,6 +291,11 @@ module Hecks
228
291
  # Whether `value` is itself the target's own (single) identity
229
292
  # value object — pure, self-contained: reads only `value` and
230
293
  # `target`, decides nothing about any particular path.
294
+ #
295
+ # @param value [Object] the offered reference value to check
296
+ # @param target [Bluebook::Aggregate] the reference's own resolved target
297
+ # @return [String, nil] the target's own single identity head, as a String,
298
+ # when `value` is that head's own declared value-object type; nil otherwise
231
299
  def direct_identity_head(value, target)
232
300
  return nil unless value.is_a?(self) && target.identity_heads.one?
233
301
 
@@ -241,6 +309,14 @@ module Hecks
241
309
  # three inputs; extracted from `reference_identity` alongside
242
310
  # `direct_identity_head` above purely to keep that method to its
243
311
  # own guard-clause shape.
312
+ #
313
+ # @param materialized [Hash, Object] the offered reference value, already
314
+ # materialized to a plain Hash (or scalar, for a path that finds nothing)
315
+ # @param path [String, Symbol] one dotted identity path to dig
316
+ # @param direct_head [String, nil] the leading segment to strip, when `path`
317
+ # restates the head `direct_identity_head` already matched
318
+ # @return [Object, nil] the value found by walking `path`'s segments; nil if
319
+ # any segment is missing or a non-Hash is dug into before the path ends
244
320
  def identity_part(materialized, path, direct_head)
245
321
  segments = path.to_s.split(".")
246
322
  segments.shift if direct_head && segments.first == direct_head
@@ -255,6 +331,12 @@ module Hecks
255
331
  end
256
332
  end
257
333
 
334
+ # Coerces a `has_many` reference-typed attribute's offered value.
335
+ #
336
+ # @param attribute [Bluebook::Attribute] the `has_many` reference attribute
337
+ # @param value [Object] the offered value; must be an Array
338
+ # @return [Array] `value`, deep-frozen and duped
339
+ # @raise [Runtime::TypeMismatch] if `value` is not an Array
258
340
  def reference_list(attribute, value)
259
341
  unless value.is_a?(Array)
260
342
  raise TypeMismatch,
@@ -264,6 +346,18 @@ module Hecks
264
346
  Freezer.deep(value.dup)
265
347
  end
266
348
 
349
+ # Normalizes an offered value into `value_object`'s own field Hash, before
350
+ # defaults, nested normalization and validation run.
351
+ #
352
+ # @param value_object [Class] the target `Bluebook::ValueObject` subclass
353
+ # @param name [String, Symbol] the attribute or argument name, quoted in a
354
+ # refusal
355
+ # @param value [Hash, Runtime::Value, Object] the offered value: a Hash of
356
+ # fields, an already-built `Value` of a differently-named but same-shaped
357
+ # type, or a bare scalar for a single-field value object
358
+ # @return [Hash{Symbol => Object}] the offered fields, keyed by attribute name
359
+ # @raise [Runtime::TypeMismatch] if `value` is a bare scalar and `value_object`
360
+ # declares more than one field
267
361
  def fields_for(value_object, name, value)
268
362
  return value.transform_keys(&:to_sym) if value.is_a?(Hash)
269
363
  # Mutations may legitimately carry a value object into a differently
@@ -316,11 +410,11 @@ module Hecks
316
410
  # a value-object-typed field of another value object staying a
317
411
  # plain Hash once stored, and this does not change that; it only
318
412
  # makes sure that Hash has the shape its own type declares.
319
- # `aggregate` is the one thing `build` didn't used to need — a
320
- # nested type can only be resolved through `aggregate.
413
+ # `aggregate` is the one thing this needs beyond `fields_for`'s own
414
+ # scope — a nested type can only be resolved through `aggregate.
321
415
  # value_object(name)`, so callers with no aggregate in reach
322
416
  # (`Value#with`, always re-setting an already-scalar arithmetic
323
- # field) simply skip this and keep their prior behavior.
417
+ # field) simply skip this and keep their own behavior unchanged.
324
418
  # Recurses into each nested field's own validation too, not only its
325
419
  # shape — found live alongside the shape bug this method's header
326
420
  # already describes: a nested `Price`/`Size` (a value-object-typed
@@ -335,6 +429,22 @@ module Hecks
335
429
  # defaults` runs first, same as the outer value object gets in
336
430
  # `build`, so a nested field's own default is filled in before its
337
431
  # own invariants read it.
432
+ #
433
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity, nil] the construct
434
+ # nested types are resolved against; a no-op if it does not respond to
435
+ # `value_object`
436
+ # @param value_object [Class] the `Bluebook::ValueObject` subclass `fields`
437
+ # belongs to
438
+ # @param fields [Hash{Symbol => Object}] the outer value object's own fields,
439
+ # already defaulted; written in place
440
+ # @return [Hash{Symbol => Object}] `fields`, with every composite-typed field
441
+ # normalized (and, for a non-list one, validated) into its own declared shape
442
+ # @raise [Runtime::TypeMismatch] if a nested field cannot be coerced to its
443
+ # declared type
444
+ # @raise [Runtime::UnknownArgument] if a nested field's own Hash names a
445
+ # field its declared type does not declare
446
+ # @raise [Runtime::InvariantViolation] if a nested field breaks one of its
447
+ # own invariants
338
448
  def normalize_composite_fields(aggregate, value_object, fields)
339
449
  return fields unless aggregate.respond_to?(:value_object)
340
450
 
@@ -372,6 +482,13 @@ module Hecks
372
482
  fields
373
483
  end
374
484
 
485
+ # Fills every declared attribute `fields` does not already hold with its
486
+ # own declared `default:`, when it has one.
487
+ #
488
+ # @param value_object [Class] the `Bluebook::ValueObject` subclass whose
489
+ # declared defaults are read
490
+ # @param fields [Hash{Symbol => Object}] the offered fields; written in place
491
+ # @return [Hash{Symbol => Object}] `fields`, with each declared default filled in
375
492
  def apply_defaults(value_object, fields)
376
493
  value_object.attributes.each_with_object(fields) do |attribute, completed|
377
494
  completed[attribute.name] = attribute.default unless completed.key?(attribute.name) || attribute.default.nil?
@@ -383,6 +500,19 @@ module Hecks
383
500
  # (every nested one), so a nested `Price`/`Size` is refused exactly
384
501
  # the same way, with exactly the same wording, as the identical type
385
502
  # declared directly on a command.
503
+ #
504
+ # @param value_object [Class] the `Bluebook::ValueObject` subclass to
505
+ # validate `fields` against
506
+ # @param fields [Hash{Symbol => Object}] the already-defaulted, already
507
+ # nested-normalized fields to check
508
+ # @return [void]
509
+ # @raise [Runtime::UnknownArgument] if `fields` names a key `value_object`
510
+ # does not declare
511
+ # @raise [Runtime::TypeMismatch] if a required field is missing, a numeric
512
+ # or pattern-constrained field is the wrong shape, or a scalar field
513
+ # arrives as a composite
514
+ # @raise [Runtime::InvariantViolation] if `fields` breaks one of
515
+ # `value_object`'s own declared invariants
386
516
  def validate!(value_object, fields)
387
517
  # C6.3 (docs/semantics/bluebook-semantics.md) — a value object is
388
518
  # validated on construction from input only; state read back from
@@ -409,6 +539,22 @@ module Hecks
409
539
  end
410
540
  end
411
541
 
542
+ # Builds one validated `Value` of `value_object`'s own type: defaults filled,
543
+ # nested fields normalized and validated, then the whole thing checked.
544
+ #
545
+ # @param value_object [Class] the `Bluebook::ValueObject` subclass to build
546
+ # @param fields [Hash{Symbol, String => Object}] the offered field values,
547
+ # either key spelling
548
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity, nil] the construct
549
+ # a nested composite field is resolved against; nil skips nested
550
+ # normalization entirely
551
+ # @return [Runtime::Value] the built, validated value object
552
+ # @raise [Runtime::UnknownArgument] if `fields` (or a nested field) names a
553
+ # key its own type does not declare
554
+ # @raise [Runtime::TypeMismatch] if a field (or a nested one) cannot be
555
+ # coerced to its declared type
556
+ # @raise [Runtime::InvariantViolation] if the built value object (or a
557
+ # nested one) breaks one of its own invariants
412
558
  def build(value_object, fields, aggregate = nil)
413
559
  fields = apply_defaults(value_object, fields.transform_keys(&:to_sym))
414
560
  fields = normalize_composite_fields(aggregate, value_object, fields)
@@ -422,10 +568,17 @@ module Hecks
422
568
  # the runtime's own callers (entity elements, the remote dispatcher's
423
569
  # `symbolize_names:` parse, Era's audit) build symbol-keyed state
424
570
  # themselves. A String key here is an adapter or caller that skipped
425
- # the codec, so it is refused by name rather than respelled: the
426
- # silent `to_sym` that used to sit here is exactly what hid such a
427
- # bypass. Always on, because it costs one `is_a?` per key, the same
428
- # as the `to_sym` it replaced.
571
+ # the codec, so it is refused by name rather than respelled: a silent
572
+ # `to_sym` here would hide exactly this kind of bypass instead. Always
573
+ # on, because it costs one `is_a?` per key, no more than a `to_sym` would.
574
+ #
575
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct
576
+ # whose declared attributes coerce `state`'s own values
577
+ # @param state [Hash{Symbol => Object}] the stored state to hydrate; every
578
+ # key must already be a Symbol
579
+ # @return [Hash{Symbol => Object}] `state`, coerced through every declared
580
+ # attribute it names
581
+ # @raise [Runtime::WiringError] if `state` holds any non-Symbol key
429
582
  def hydrate(aggregate, state)
430
583
  undecoded = state.keys.grep_v(Symbol)
431
584
  unless undecoded.empty?
@@ -444,6 +597,15 @@ module Hecks
444
597
 
445
598
  TRUSTED_LOAD_KEY = :hecks_trusting_stored_state
446
599
 
600
+ # Marks the block as loading trusted, already-validated stored state, so
601
+ # `validate!` skips its own checks for the block's duration.
602
+ #
603
+ # `Thread.current`-backed, not a plain ivar, so two threads hydrating
604
+ # concurrently on the same `Value` singleton class never see or clear
605
+ # each other's flag.
606
+ #
607
+ # @yield the code that should see `trusting_stored_state?` true
608
+ # @return [Object] the block's result
447
609
  def trusting_stored_state
448
610
  previous = Thread.current[TRUSTED_LOAD_KEY]
449
611
  Thread.current[TRUSTED_LOAD_KEY] = true
@@ -452,6 +614,10 @@ module Hecks
452
614
  Thread.current[TRUSTED_LOAD_KEY] = previous
453
615
  end
454
616
 
617
+ # Reports whether the current thread is inside a `trusting_stored_state` block.
618
+ #
619
+ # @return [Boolean] true if a `trusting_stored_state` block is on this
620
+ # thread's own call stack
455
621
  def trusting_stored_state? = Thread.current[TRUSTED_LOAD_KEY] == true
456
622
 
457
623
  # QualityControl BUG#125 — the one narrow door `check_scalar_shapes`
@@ -494,6 +660,16 @@ module Hecks
494
660
  # field.
495
661
  BOOTSTRAP_KEY = :hecks_judge_bootstrapping
496
662
 
663
+ # Marks the block as `MetaValidator::Judge#send_to`'s own self-hosted
664
+ # bootstrap dispatch, so `check_scalar_shapes` loosens its `String` check
665
+ # for the block's duration.
666
+ #
667
+ # `Thread.current`-backed, not a plain ivar, so two threads bootstrapping
668
+ # concurrently on the same `Value` singleton class never see or clear
669
+ # each other's flag.
670
+ #
671
+ # @yield the code that should see `judge_bootstrapping?` true
672
+ # @return [Object] the block's result
497
673
  def judge_bootstrapping
498
674
  previous = Thread.current[BOOTSTRAP_KEY]
499
675
  Thread.current[BOOTSTRAP_KEY] = true
@@ -502,23 +678,23 @@ module Hecks
502
678
  Thread.current[BOOTSTRAP_KEY] = previous
503
679
  end
504
680
 
681
+ # Reports whether the current thread is inside a `judge_bootstrapping` block.
682
+ #
683
+ # @return [Boolean] true if a `judge_bootstrapping` block is on this
684
+ # thread's own call stack
505
685
  def judge_bootstrapping? = Thread.current[BOOTSTRAP_KEY] == true
506
686
 
507
- # `Value.identifier` used to live here: hand it a one-field value object
508
- # and it opened it, so `identified_by :number` could pass for an identity
509
- # and the runtime would guess which field was meant. That guess is gone.
510
687
  # An identity names its field — `identified_by :number` — and the
511
- # path is what reaches the scalar. A declaration that names no field is
512
- # refused when the bluebook loads, so nothing has to be unwrapped later.
688
+ # path is what reaches the scalar, never a guess at which field a
689
+ # one-field value object might mean. A declaration that names no
690
+ # field is refused when the bluebook loads, so nothing has to be
691
+ # unwrapped later.
513
692
  #
514
693
  # `scalar` below is a different job and stays: rendering a value object
515
694
  # into a column or a message, where there is no path to consult.
516
695
 
517
- # `Value.reference_id` lived here, opening a reference to find the id
518
- # inside it. A reference is the id now — refused at the payload gate if it
519
- # arrives as anything else — so there is nothing left to open. The comment
520
- # it carried said retiring it meant changing how references are stored ;
521
- # that is what happened.
696
+ # A reference is the id itself — refused at the payload gate if it
697
+ # arrives as anything else — so there is nothing to open inside it.
522
698
 
523
699
  # A reference is an ID, so anything else is not one.
524
700
  #
@@ -534,9 +710,9 @@ module Hecks
534
710
  #
535
711
  # Widened past the object shape by BUG#27 (QualityControl ledger,
536
712
  # found live on `qa/stress_domains/referral_chain`'s `Member.Join`/
537
- # `Referral.Issue`). A bare Boolean, Array, or `null` used to sail
538
- # through here untouched — nothing but Hash/Value ever refused —
539
- # then get `.to_s`'d into a lookup key by `CommandRules::
713
+ # `Referral.Issue`). Without this widening, a bare Boolean, Array, or
714
+ # `null` would sail through here untouched — nothing but Hash/Value
715
+ # ever refused — then get `.to_s`'d into a lookup key by `CommandRules::
540
716
  # References#reference_key` ("true", "false", "[8, 8]") and answer
541
717
  # NotFound, or, for `null`, skip the lookup outright
542
718
  # (`next if held.nil?`, command_rules/references.rb) and let the
@@ -569,6 +745,16 @@ module Hecks
569
745
  # today, and inventing a rule for a shape the language cannot
570
746
  # declare is how decoration gets written. `reference_list` (below)
571
747
  # already owns "not an Array at all" for that case.
748
+ #
749
+ # @param command [Class] the command class (`Bluebook::Command` subclass)
750
+ # `attribute` is declared on, named in a refusal
751
+ # @param attribute [Bluebook::Attribute] the attribute to check; a no-op
752
+ # unless it is reference-typed
753
+ # @param value [Object] the offered value
754
+ # @return [void]
755
+ # @raise [Runtime::TypeMismatch] if `value` (or, for a `has_many` reference,
756
+ # any of its elements) is a Hash or a `Runtime::Value` rather than a plain
757
+ # identity, or a required scalar reference is anything but a String
572
758
  def refuse_object_reference(command, attribute, value)
573
759
  return unless attribute.reference?
574
760
 
@@ -596,6 +782,10 @@ module Hecks
596
782
  # `false`, `nil`, `[8, 8]` — the same rendering every other
597
783
  # TypeMismatch in this file already uses for "here is what you
598
784
  # actually sent."
785
+ #
786
+ # @param value [Object] the wrongly-shaped offered value to describe
787
+ # @return [String] `"an object"` for a Hash or `Runtime::Value`; otherwise
788
+ # `Rendering.describe(value)`
599
789
  def reference_shape_description(value)
600
790
  return "an object" if value.is_a?(Hash) || value.is_a?(self)
601
791
 
@@ -613,6 +803,11 @@ module Hecks
613
803
  # identity has two parts — so a composite target fell through the guard
614
804
  # and the refusal went silent exactly where it had the most to say. A
615
805
  # single-path target reads as it always did.
806
+ #
807
+ # @param attribute [Bluebook::Attribute] the reference-typed attribute to
808
+ # describe the target's own identity heads for
809
+ # @return [String] `" (Target is known by head1, head2)"`, or `""` when the
810
+ # target cannot be resolved or declares no identity heads
616
811
  def known_by(attribute)
617
812
  heads = Array(attribute.type.resolve&.identity_heads)
618
813
  return "" if heads.empty?
@@ -620,6 +815,15 @@ module Hecks
620
815
  " (#{attribute.type.target_name} is known by #{heads.join(', ')})"
621
816
  end
622
817
 
818
+ # Renders a value object into the bare scalar its one field holds — for a
819
+ # column or a message, where there is no path to consult.
820
+ #
821
+ # @param value [Object] the value to render; passed through unless a
822
+ # `Runtime::Value`
823
+ # @return [Object] `value` unchanged when it is not a `Runtime::Value`;
824
+ # otherwise its one field's own value
825
+ # @raise [Runtime::TypeMismatch] if `value` is a `Runtime::Value` with more
826
+ # than one field
623
827
  def scalar(value)
624
828
  return value unless value.is_a?(self)
625
829
 
@@ -629,6 +833,19 @@ module Hecks
629
833
  raise TypeMismatch, RefusalWording.render_site("TypeMismatch", "multi_field_scalar", type: value.type_name)
630
834
  end
631
835
 
836
+ # Coerces a derived identity string back into `attribute`'s own declared type.
837
+ #
838
+ # @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct
839
+ # `attribute` is declared on
840
+ # @param attribute [Bluebook::Attribute] the identity attribute to coerce
841
+ # `identifier` for
842
+ # @param identifier [String, Object] the derived identity, typically a
843
+ # String (`Identity.of`/`Identity.from`'s own return)
844
+ # @return [Runtime::Value, String, Object] a built value object when
845
+ # `attribute`'s type names a single-field value object; `identifier`
846
+ # unchanged otherwise
847
+ # @raise [Runtime::TypeMismatch] if `attribute`'s type names a value object
848
+ # with more than one field
632
849
  def from_identifier(aggregate, attribute, identifier)
633
850
  value_object = value_object_for(aggregate, attribute.type)
634
851
  return identifier unless value_object
@@ -679,6 +896,11 @@ module Hecks
679
896
  identifier
680
897
  end
681
898
 
899
+ # Renders a value object's fields as a canonical JSON string, for an
900
+ # invariant refusal to quote.
901
+ #
902
+ # @param fields [Hash{Symbol, String => Object}] the field values to render
903
+ # @return [String] `fields`, sorted by key name and JSON-encoded
682
904
  def canonical_fields(fields)
683
905
  JSON.generate(fields.sort_by { |name, _| name.to_s }.to_h)
684
906
  end
@@ -798,10 +1020,10 @@ module Hecks
798
1020
  # nil" — the identical string the Rust side's generated `from_json`
799
1021
  # gives the same input, so the corpus can pin it on both. Checked
800
1022
  # first among the field-content checks (after `check_unknown_fields`'s
801
- # own structural gate above, BUG#41): an invariant reading a field
802
- # that never arrived is exactly the thing that used to answer
803
- # "invariant violated" (or nothing at all — `ToppingName`'s
804
- # `{value: null}` used to be accepted and stored). A `default:` has
1023
+ # own structural gate above, BUG#41): without this, an invariant
1024
+ # reading a field that never arrived would be exactly the thing that
1025
+ # answers "invariant violated" (or nothing at all — `ToppingName`'s
1026
+ # `{value: null}` would be accepted and stored). A `default:` has
805
1027
  # already been filled in by `apply_defaults`; a list field's absence
806
1028
  # is an empty list, never a refusal.
807
1029
  private def check_required_fields(value_object, fields)
@@ -817,7 +1039,7 @@ module Hecks
817
1039
  end
818
1040
 
819
1041
  # Checked before invariants, because an invariant reading a mistyped field
820
- # is exactly the thing that used to explode.
1042
+ # is exactly the thing that would otherwise explode.
821
1043
  NUMERIC = { "Integer" => Integer, "Float" => Numeric }.freeze
822
1044
  private def check_numeric_fields(value_object, fields)
823
1045
  value_object.attributes.each do |attribute|
@@ -865,12 +1087,12 @@ module Hecks
865
1087
  # true/false) — QualityControl BUG#125, matching Rust's generated
866
1088
  # `from_json`, which requires a JSON string node for a String-typed
867
1089
  # field unconditionally and refuses anything else, including a JSON
868
- # number or boolean. Ruby used to tolerate exactly that (found live:
869
- # `Chess::Piece.Capture`'s `PieceId`, String-typed, offered a bignum
870
- # `id` — Ruby let it pass and failed later on an unrelated field,
871
- # Rust refused on `id` itself, immediately) — no longer, except
1090
+ # number or boolean. Without this, Ruby would tolerate exactly that
1091
+ # (found live: `Chess::Piece.Capture`'s `PieceId`, String-typed,
1092
+ # offered a bignum `id` — Ruby let it pass and failed later on an
1093
+ # unrelated field, Rust refused on `id` itself, immediately) — except
872
1094
  # inside `judge_bootstrapping?` (above), the one caller genuinely
873
- # relying on the old leniency; see that flag's own comment for why.
1095
+ # relying on that leniency; see that flag's own comment for why.
874
1096
  #
875
1097
  # `TrueClass`/`FalseClass` stay laxer than `check_numeric_fields`
876
1098
  # above: for those two, this still only enforces that the shape